From 00c14cd88c30d34640b15c47a1a56f9fa1cc827b Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 17 Apr 2026 16:40:00 -0700 Subject: [PATCH 01/94] Add local-only uf2reset CLI command --- docs/cli_commands.md | 10 ++++++++++ src/helpers/CommonCLI.cpp | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 0e785f4e83..57ab6c6459 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -30,6 +30,16 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +### Enter the UF2 bootloader (nRF52 only) +**Usage:** +- `uf2reset` + +**Serial Only:** Yes + +**Note:** Reboots directly into the UF2 bootloader on supported nRF52 boards. + +--- + ### Reset the clock and reboot **Usage:** - `clkreboot` diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 2f7a0fffcb..ab035f6623 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -4,6 +4,29 @@ #include "AdvertDataHelpers.h" #include +#if defined(NRF52_PLATFORM) +#include +#include + +#ifndef DFU_MAGIC_UF2_RESET +#define DFU_MAGIC_UF2_RESET 0x57 +#endif + +static void resetToUf2Bootloader() { + uint8_t sd_enabled = 0; + sd_softdevice_is_enabled(&sd_enabled); + + if (sd_enabled) { + sd_power_gpregret_clr(0, 0xFF); + sd_power_gpregret_set(0, DFU_MAGIC_UF2_RESET); + } else { + NRF_POWER->GPREGRET = DFU_MAGIC_UF2_RESET; + } + + NVIC_SystemReset(); +} +#endif + #ifndef BRIDGE_MAX_BAUD #define BRIDGE_MAX_BAUD 115200 #endif @@ -210,6 +233,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch _board->powerOff(); // doesn't return } else if (memcmp(command, "reboot", 6) == 0) { _board->reboot(); // doesn't return + } else if (sender_timestamp == 0 && memcmp(command, "uf2reset", 8) == 0 && (command[8] == 0 || command[8] == ' ')) { // from serial command line only +#if defined(NRF52_PLATFORM) + resetToUf2Bootloader(); // doesn't return +#else + strcpy(reply, "ERR: unsupported"); +#endif } else if (memcmp(command, "clkreboot", 9) == 0) { // Reset clock getRTCClock()->setCurrentTime(1715770351); // 15 May 2024, 8:50pm From 0ce1a550178d82a569c0f4d998a3c576bf549f92 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 20 Apr 2026 13:40:07 -0700 Subject: [PATCH 02/94] Refactor build.sh menu and build flow --- build.sh | 764 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 615 insertions(+), 149 deletions(-) diff --git a/build.sh b/build.sh index 313c4c47a0..cd45e8d8d8 100755 --- a/build.sh +++ b/build.sh @@ -1,9 +1,41 @@ #!/usr/bin/env bash +ALL_PIO_ENVS=() +PIO_CONFIG_JSON="" +MENU_CHOICE="" +SELECTED_TARGET="" + +ENV_VARIANT_SUFFIX_PATTERN='companion_radio_serial|companion_radio_wifi|companion_radio_usb|comp_radio_usb|companion_usb|companion_radio_ble|companion_ble|repeater_bridge_rs232_serial1|repeater_bridge_rs232_serial2|repeater_bridge_rs232|repeater_bridge_espnow|terminal_chat|room_server|room_svr|kiss_modem|sensor|repeatr|repeater' +BOARD_MODIFIER_WITHOUT_DISPLAY="_without_display" +BOARD_MODIFIER_LOGGING="_logging" +BOARD_MODIFIER_TFT="_tft" +BOARD_MODIFIER_EINK="_eink" +BOARD_MODIFIER_EINK_SUFFIX="Eink" +BOARD_LABEL_WITHOUT_DISPLAY="without_display" +BOARD_LABEL_LOGGING="logging" +BOARD_LABEL_TFT="tft" +BOARD_LABEL_EINK="eink" +DEFAULT_VARIANT_LABEL="default" +TAG_PREFIX_ROOM_SERVER="room-server" +TAG_PREFIX_COMPANION="companion" +TAG_PREFIX_REPEATER="repeater" +BULK_BUILD_SUFFIX_REPEATER="_repeater" +BULK_BUILD_SUFFIX_COMPANION_USB="_companion_radio_usb" +BULK_BUILD_SUFFIX_COMPANION_BLE="_companion_radio_ble" +BULK_BUILD_SUFFIX_ROOM_SERVER="_room_server" +SUPPORTED_PLATFORM_PATTERN='ESP32_PLATFORM|NRF52_PLATFORM|STM32_PLATFORM|RP2040_PLATFORM' +OUTPUT_DIR="out" +FALLBACK_VERSION_PREFIX="dev" +FALLBACK_VERSION_DATE_FORMAT='+%Y-%m-%d-%H-%M' + +# External programs invoked by this script: +# bash, cat, cp, date, git, grep, head, mkdir, pio, python3, rm, sed, sort +# Keep this list in sync when adding or removing non-builtin command usage. + global_usage() { cat - < [target] +bash build.sh [target] Commands: help|usage|-h|--help: Shows this message. @@ -17,21 +49,26 @@ Commands: Examples: Build firmware for the "RAK_4631_repeater" device target -$ sh build.sh build-firmware RAK_4631_repeater +$ bash build.sh build-firmware RAK_4631_repeater + +Run without arguments to choose a target from an interactive menu +$ bash build.sh Build all firmwares for device targets containing the string "RAK_4631" -$ sh build.sh build-matching-firmwares +$ bash build.sh build-matching-firmwares Build all companion firmwares -$ sh build.sh build-companion-firmwares +$ bash build.sh build-companion-firmwares Build all repeater firmwares -$ sh build.sh build-repeater-firmwares +$ bash build.sh build-repeater-firmwares Build all chat room server firmwares -$ sh build.sh build-room-server-firmwares +$ bash build.sh build-room-server-firmwares Environment Variables: + FIRMWARE_VERSION=vX.Y.Z: Firmware version to embed in the build output. + If not set, build.sh derives a default from the latest matching git tag and appends "-dev". DISABLE_DEBUG=1: Disables all debug logging flags (MESH_DEBUG, MESH_PACKET_LOGGING, etc.) If not set, debug flags from variant platformio.ini files are used. @@ -39,60 +76,425 @@ Examples: Build without debug logging: $ export FIRMWARE_VERSION=v1.0.0 $ export DISABLE_DEBUG=1 -$ sh build.sh build-firmware RAK_4631_repeater +$ bash build.sh build-firmware RAK_4631_repeater Build with debug logging (default, uses flags from variant files): $ export FIRMWARE_VERSION=v1.0.0 -$ sh build.sh build-firmware RAK_4631_repeater +$ bash build.sh build-firmware RAK_4631_repeater + +Build with the derived default version from git tags: +$ unset FIRMWARE_VERSION +$ bash build.sh EOF } -# get a list of pio env names that start with "env:" +init_project_context() { + if [ ${#ALL_PIO_ENVS[@]} -eq 0 ]; then + mapfile -t ALL_PIO_ENVS < <(pio project config | grep 'env:' | sed 's/env://') + fi + + if [ -z "$PIO_CONFIG_JSON" ]; then + PIO_CONFIG_JSON=$(pio project config --json-output) + fi +} + get_pio_envs() { - pio project config | grep 'env:' | sed 's/env://' + if [ ${#ALL_PIO_ENVS[@]} -gt 0 ]; then + printf '%s\n' "${ALL_PIO_ENVS[@]}" + else + pio project config | grep 'env:' | sed 's/env://' + fi +} + +canonicalize_variant_suffix() { + local variant_suffix=$1 + + case "${variant_suffix,,}" in + comp_radio_usb|companion_usb|companion_radio_usb) + echo "companion_radio_usb" + ;; + companion_ble|companion_radio_ble) + echo "companion_radio_ble" + ;; + room_svr|room_server) + echo "room_server" + ;; + repeatr|repeater) + echo "repeater" + ;; + *) + echo "${variant_suffix,,}" + ;; + esac +} + +trim_trailing_underscores() { + local value=$1 + + while [[ "$value" == *_ ]]; do + value=${value%_} + done + + echo "$value" +} + +sort_lines_case_insensitive() { + sort -f +} + +print_numbered_menu() { + local items=("$@") + local i + + for i in "${!items[@]}"; do + printf '%d) %s\n' "$((i + 1))" "${items[$i]}" + done +} + +prompt_menu_choice() { + local prompt_label=$1 + local max_choice=$2 + local allow_back=${3:-0} + local choice + + while true; do + if [ "$allow_back" -eq 1 ]; then + read -r -p "${prompt_label} [1-${max_choice}, B=Back, Q=Quit]: " choice + else + read -r -p "${prompt_label} [1-${max_choice}, Q=Quit]: " choice + fi + + case "${choice^^}" in + Q) + MENU_CHOICE="QUIT" + return 0 + ;; + B) + if [ "$allow_back" -eq 1 ]; then + MENU_CHOICE="BACK" + return 0 + fi + echo "Invalid selection." + ;; + *) + if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "$max_choice" ]; then + MENU_CHOICE="$choice" + return 0 + fi + echo "Invalid selection." + ;; + esac + done +} + +get_env_metadata() { + local env_name=$1 + local trimmed_env_name + local board_part + local variant_part + local board_family + local board_modifier + local variant_label + local tag_prefix + + trimmed_env_name=$(trim_trailing_underscores "$env_name") + board_part=$trimmed_env_name + variant_part="" + + shopt -s nocasematch + # Split a raw env name into board and variant pieces using the normalized + # suffix vocabulary defined near the top of the file. + if [[ "$trimmed_env_name" =~ ^(.+)[_-](${ENV_VARIANT_SUFFIX_PATTERN})$ ]]; then + board_part=${BASH_REMATCH[1]} + variant_part=$(canonicalize_variant_suffix "${BASH_REMATCH[2]}") + fi + + # Fold display and form-factor suffixes into the variant label so related + # boards share one first-level menu entry. + case "$board_part" in + *"$BOARD_MODIFIER_WITHOUT_DISPLAY") + board_family=${board_part%"$BOARD_MODIFIER_WITHOUT_DISPLAY"} + board_modifier="$BOARD_LABEL_WITHOUT_DISPLAY" + ;; + *"$BOARD_MODIFIER_LOGGING") + board_family=${board_part%"$BOARD_MODIFIER_LOGGING"} + board_modifier="$BOARD_LABEL_LOGGING" + ;; + *"$BOARD_MODIFIER_TFT") + board_family=${board_part%"$BOARD_MODIFIER_TFT"} + board_modifier="$BOARD_LABEL_TFT" + ;; + *"$BOARD_MODIFIER_EINK") + board_family=${board_part%"$BOARD_MODIFIER_EINK"} + board_modifier="$BOARD_LABEL_EINK" + ;; + *"$BOARD_MODIFIER_EINK_SUFFIX") + board_family=${board_part%"$BOARD_MODIFIER_EINK_SUFFIX"} + board_modifier="$BOARD_LABEL_EINK" + ;; + *) + board_family=$board_part + board_modifier="" + ;; + esac + shopt -u nocasematch + + variant_label="$variant_part" + if [ -n "$board_modifier" ]; then + if [ -n "$variant_label" ]; then + variant_label="${board_modifier}_${variant_label}" + else + variant_label="$board_modifier" + fi + fi + + if [ -z "$variant_label" ]; then + variant_label="$DEFAULT_VARIANT_LABEL" + fi + + case "$variant_part" in + room_server) + tag_prefix="$TAG_PREFIX_ROOM_SERVER" + ;; + companion_radio_*) + tag_prefix="$TAG_PREFIX_COMPANION" + ;; + repeater*) + tag_prefix="$TAG_PREFIX_REPEATER" + ;; + *) + tag_prefix="" + ;; + esac + + printf '%s\t%s\t%s\n' "$board_family" "$variant_label" "$tag_prefix" +} + +get_metadata_field() { + local env_name=$1 + local field_index=$2 + local metadata + + metadata=$(get_env_metadata "$env_name") + case "$field_index" in + 1) + echo "${metadata%%$'\t'*}" + ;; + 2) + metadata=${metadata#*$'\t'} + echo "${metadata%%$'\t'*}" + ;; + 3) + echo "${metadata##*$'\t'}" + ;; + esac } -# Catch cries for help before doing anything else. -case $1 in - help|usage|-h|--help) +get_board_family_for_env() { + get_metadata_field "$1" 1 +} + +get_variant_name_for_env() { + get_metadata_field "$1" 2 +} + +get_release_tag_prefix_for_env() { + get_metadata_field "$1" 3 +} + +get_variants_for_board() { + local board_family=$1 + local env + + for env in "${ALL_PIO_ENVS[@]}"; do + if [ "$(get_board_family_for_env "$env")" == "$board_family" ]; then + echo "$env" + fi + done | sort_lines_case_insensitive +} + +prompt_for_variant_for_board() { + local board=$1 + local -A seen_variant_labels=() + local variants + local variant_labels + local i + local j + + mapfile -t variants < <(get_variants_for_board "$board") + if [ ${#variants[@]} -eq 0 ]; then + echo "No firmware variants were found for ${board}." + return 1 + fi + + if [ ${#variants[@]} -eq 1 ]; then + SELECTED_TARGET="${variants[0]}" + return 0 + fi + + variant_labels=() + for i in "${!variants[@]}"; do + variant_labels[i]=$(get_variant_name_for_env "${variants[$i]}") + seen_variant_labels["${variant_labels[$i]}"]=$(( ${seen_variant_labels["${variant_labels[$i]}"]:-0} + 1 )) + done + + # Stop early if normalization would present the user with ambiguous labels. + for i in "${!variant_labels[@]}"; do + if [ "${seen_variant_labels["${variant_labels[$i]}"]}" -gt 1 ]; then + echo "Ambiguous firmware variants detected for ${board}: ${variant_labels[$i]}" + echo "The normalized menu labels are not unique for this board family." + for j in "${!variants[@]}"; do + echo " ${variants[$j]}" + done + exit 1 + fi + done + + echo "Select a firmware variant for ${board}:" + while true; do + print_numbered_menu "${variant_labels[@]}" + prompt_menu_choice "Variant selection" "${#variant_labels[@]}" 1 + if [ "$MENU_CHOICE" == "BACK" ]; then + return 1 + fi + if [ "$MENU_CHOICE" == "QUIT" ]; then + echo "Cancelled." + exit 1 + fi + + SELECTED_TARGET="${variants[$((MENU_CHOICE - 1))]}" + return 0 + done +} + +prompt_for_board_target() { + local -A seen_boards=() + local boards=() + local board + local env + + if ! [ -t 0 ]; then + echo "No command provided and no interactive terminal is available." global_usage exit 1 - ;; - list|-l) - get_pio_envs - exit 0 - ;; -esac + fi -# cache project config json for use in get_platform_for_env() -PIO_CONFIG_JSON=$(pio project config --json-output) + if [ ${#ALL_PIO_ENVS[@]} -eq 0 ]; then + echo "No PlatformIO environments were found." + exit 1 + fi + + for env in "${ALL_PIO_ENVS[@]}"; do + board=$(get_board_family_for_env "$env") + if [ -z "${seen_boards[$board]}" ]; then + seen_boards["$board"]=1 + boards+=("$board") + fi + done + + mapfile -t boards < <(printf '%s\n' "${boards[@]}" | sort_lines_case_insensitive) + + echo "No command provided. Select a board family:" + while true; do + print_numbered_menu "${boards[@]}" + prompt_menu_choice "Board selection" "${#boards[@]}" + if [ "$MENU_CHOICE" == "QUIT" ]; then + echo "Cancelled." + exit 1 + fi + + board=${boards[$((MENU_CHOICE - 1))]} + if prompt_for_variant_for_board "$board"; then + echo "Building firmware for ${SELECTED_TARGET}" + return 0 + fi + done +} + +get_latest_version_from_tags() { + local env_name=$1 + local tag_prefix + local latest_tag + local fallback_version + + fallback_version="${FALLBACK_VERSION_PREFIX}-$(date "${FALLBACK_VERSION_DATE_FORMAT}")" + tag_prefix=$(get_release_tag_prefix_for_env "$env_name") + if [ -z "$tag_prefix" ]; then + echo "$fallback_version" + return 0 + fi + + latest_tag=$(git tag --list "${tag_prefix}-v*" --sort=-version:refname | head -n 1) + if [ -z "$latest_tag" ]; then + echo "$fallback_version" + return 0 + fi + + echo "${latest_tag#"${tag_prefix}"-}" +} + +derive_default_firmware_version() { + local env_name=$1 + local base_version + + base_version=$(get_latest_version_from_tags "$env_name") + case "$base_version" in + *-dev|dev-*) + echo "$base_version" + ;; + *) + echo "${base_version}-dev" + ;; + esac +} + +prompt_for_firmware_version() { + local env_name=$1 + local suggested_version + local entered_version + + suggested_version=$(derive_default_firmware_version "$env_name") + + if ! [ -t 0 ]; then + FIRMWARE_VERSION="$suggested_version" + echo "FIRMWARE_VERSION not set, using derived default: ${FIRMWARE_VERSION}" + return 0 + fi + + echo "Suggested firmware version for ${env_name}: ${suggested_version}" + read -r -e -i "${suggested_version}" -p "Firmware version: " entered_version + FIRMWARE_VERSION="${entered_version:-$suggested_version}" +} -# $1 should be the string to find (case insensitive) get_pio_envs_containing_string() { + local env + shopt -s nocasematch - envs=($(get_pio_envs)) - for env in "${envs[@]}"; do - if [[ "$env" == *${1}* ]]; then - echo $env - fi + for env in "${ALL_PIO_ENVS[@]}"; do + if [[ "$env" == *${1}* ]]; then + echo "$env" + fi done + shopt -u nocasematch } -# $1 should be the string to find (case insensitive) get_pio_envs_ending_with_string() { + local env + shopt -s nocasematch - envs=($(get_pio_envs)) - for env in "${envs[@]}"; do + for env in "${ALL_PIO_ENVS[@]}"; do if [[ "$env" == *${1} ]]; then - echo $env + echo "$env" fi done + shopt -u nocasematch } -# get platform flag for a given environment -# $1 should be the environment name get_platform_for_env() { local env_name=$1 + + # PlatformIO exposes project config as JSON; scan the selected env's + # build_flags to recover the platform token used for artifact collection. echo "$PIO_CONFIG_JSON" | python3 -c " import sys, json, re data = json.load(sys.stdin) @@ -101,142 +503,154 @@ for section, options in data: for key, value in options: if key == 'build_flags': for flag in value: - match = re.search(r'(ESP32_PLATFORM|NRF52_PLATFORM|STM32_PLATFORM|RP2040_PLATFORM)', flag) + match = re.search(r'($SUPPORTED_PLATFORM_PATTERN)', flag) if match: print(match.group(1)) sys.exit(0) " } -# disable all debug logging flags if DISABLE_DEBUG=1 is set +is_supported_platform() { + local env_platform=$1 + + [[ "$env_platform" =~ ^(${SUPPORTED_PLATFORM_PATTERN})$ ]] +} + disable_debug_flags() { if [ "$DISABLE_DEBUG" == "1" ]; then export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_DEBUG -UBLE_DEBUG_LOGGING -UWIFI_DEBUG_LOGGING -UBRIDGE_DEBUG -UGPS_NMEA_DEBUG -UCORE_DEBUG_LEVEL -UESPNOW_DEBUG_LOGGING -UDEBUG_RP2040_WIRE -UDEBUG_RP2040_SPI -UDEBUG_RP2040_CORE -UDEBUG_RP2040_PORT -URADIOLIB_DEBUG_SPI -UCFG_DEBUG -URADIOLIB_DEBUG_BASIC -URADIOLIB_DEBUG_PROTOCOL" fi } -# build firmware for the provided pio env in $1 -build_firmware() { - # get env platform for post build actions - ENV_PLATFORM=($(get_platform_for_env $1)) +copy_build_output() { + local source_path=$1 + local output_path=$2 - # get git commit sha - COMMIT_HASH=$(git rev-parse --short HEAD) + if [ -f "$source_path" ]; then + cp -- "$source_path" "$output_path" + fi +} - # set firmware build date - FIRMWARE_BUILD_DATE=$(date '+%d-%b-%Y') +collect_esp32_artifacts() { + local env_name=$1 + local firmware_filename=$2 - # get FIRMWARE_VERSION, which should be provided by the environment - if [ -z "$FIRMWARE_VERSION" ]; then - echo "FIRMWARE_VERSION must be set in environment" - exit 1 - fi + pio run -t mergebin -e "$env_name" + copy_build_output ".pio/build/${env_name}/firmware.bin" "out/${firmware_filename}.bin" + copy_build_output ".pio/build/${env_name}/firmware-merged.bin" "out/${firmware_filename}-merged.bin" +} - # set firmware version string - # e.g: v1.0.0-abcdef - FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}-${COMMIT_HASH}" +collect_nrf52_artifacts() { + local env_name=$1 + local firmware_filename=$2 - # craft filename - # e.g: RAK_4631_Repeater-v1.0.0-SHA - FIRMWARE_FILENAME="$1-${FIRMWARE_VERSION_STRING}" + python3 bin/uf2conv/uf2conv.py ".pio/build/${env_name}/firmware.hex" -c -o ".pio/build/${env_name}/firmware.uf2" -f 0xADA52840 + copy_build_output ".pio/build/${env_name}/firmware.uf2" "out/${firmware_filename}.uf2" + copy_build_output ".pio/build/${env_name}/firmware.zip" "out/${firmware_filename}.zip" +} - # add firmware version info to end of existing platformio build flags in environment vars - export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DFIRMWARE_BUILD_DATE='\"${FIRMWARE_BUILD_DATE}\"' -DFIRMWARE_VERSION='\"${FIRMWARE_VERSION_STRING}\"'" +collect_stm32_artifacts() { + local env_name=$1 + local firmware_filename=$2 - # disable debug flags if requested - disable_debug_flags + copy_build_output ".pio/build/${env_name}/firmware.bin" "out/${firmware_filename}.bin" + copy_build_output ".pio/build/${env_name}/firmware.hex" "out/${firmware_filename}.hex" +} - # build firmware target - pio run -e $1 +collect_rp2040_artifacts() { + local env_name=$1 + local firmware_filename=$2 - # build merge-bin for esp32 fresh install, copy .bins to out folder (e.g: Heltec_v3_room_server-v1.0.0-SHA.bin) - if [ "$ENV_PLATFORM" == "ESP32_PLATFORM" ]; then - pio run -t mergebin -e $1 - cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true - cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-merged.bin 2>/dev/null || true - fi + copy_build_output ".pio/build/${env_name}/firmware.bin" "out/${firmware_filename}.bin" + copy_build_output ".pio/build/${env_name}/firmware.uf2" "out/${firmware_filename}.uf2" +} - # build .uf2 for nrf52 boards, copy .uf2 and .zip to out folder (e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2) - if [ "$ENV_PLATFORM" == "NRF52_PLATFORM" ]; then - python3 bin/uf2conv/uf2conv.py .pio/build/$1/firmware.hex -c -o .pio/build/$1/firmware.uf2 -f 0xADA52840 - cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true - cp .pio/build/$1/firmware.zip out/${FIRMWARE_FILENAME}.zip 2>/dev/null || true - fi +collect_build_artifacts() { + local env_name=$1 + local env_platform=$2 + local firmware_filename=$3 + + # Post-build outputs differ by platform, so dispatch to the matching + # collector after the main firmware build succeeds. + case "$env_platform" in + ESP32_PLATFORM) + collect_esp32_artifacts "$env_name" "$firmware_filename" + ;; + NRF52_PLATFORM) + collect_nrf52_artifacts "$env_name" "$firmware_filename" + ;; + STM32_PLATFORM) + collect_stm32_artifacts "$env_name" "$firmware_filename" + ;; + RP2040_PLATFORM) + collect_rp2040_artifacts "$env_name" "$firmware_filename" + ;; + esac +} - # for stm32, copy .bin and .hex to out folder - if [ "$ENV_PLATFORM" == "STM32_PLATFORM" ]; then - cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true - cp .pio/build/$1/firmware.hex out/${FIRMWARE_FILENAME}.hex 2>/dev/null || true +build_firmware() { + local env_name=$1 + local env_platform + local commit_hash + local firmware_build_date + local firmware_version_string + local firmware_filename + + env_platform=$(get_platform_for_env "$env_name") + if ! is_supported_platform "$env_platform"; then + echo "Unsupported or unknown platform for env: $env_name" + exit 1 fi - # for rp2040, copy .bin and .uf2 to out folder - if [ "$ENV_PLATFORM" == "RP2040_PLATFORM" ]; then - cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true - cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true + commit_hash=$(git rev-parse --short HEAD) + firmware_build_date=$(date '+%d-%b-%Y') + + if [ -z "$FIRMWARE_VERSION" ]; then + prompt_for_firmware_version "$env_name" + echo "Using firmware version: ${FIRMWARE_VERSION}" fi + firmware_version_string="${FIRMWARE_VERSION}-${commit_hash}" + firmware_filename="${env_name}-${firmware_version_string}" + + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DFIRMWARE_BUILD_DATE='\"${firmware_build_date}\"' -DFIRMWARE_VERSION='\"${firmware_version_string}\"'" + disable_debug_flags + + pio run -e "$env_name" + collect_build_artifacts "$env_name" "$env_platform" "$firmware_filename" } -# firmwares containing $1 will be built build_all_firmwares_matching() { - envs=($(get_pio_envs_containing_string "$1")) + local envs + local env + + mapfile -t envs < <(get_pio_envs_containing_string "$1") for env in "${envs[@]}"; do - build_firmware $env + build_firmware "$env" done } -# firmwares ending with $1 will be built build_all_firmwares_by_suffix() { - envs=($(get_pio_envs_ending_with_string "$1")) + local envs + local env + + mapfile -t envs < <(get_pio_envs_ending_with_string "$1") for env in "${envs[@]}"; do - build_firmware $env + build_firmware "$env" done } build_repeater_firmwares() { - -# # build specific repeater firmwares -# build_firmware "Heltec_v2_repeater" -# build_firmware "Heltec_v3_repeater" -# build_firmware "Xiao_C3_Repeater_sx1262" -# build_firmware "Xiao_S3_WIO_Repeater" -# build_firmware "LilyGo_T3S3_sx1262_Repeater" -# build_firmware "RAK_4631_Repeater" - - # build all repeater firmwares - build_all_firmwares_by_suffix "_repeater" - + build_all_firmwares_by_suffix "$BULK_BUILD_SUFFIX_REPEATER" } build_companion_firmwares() { - -# # build specific companion firmwares -# build_firmware "Heltec_v2_companion_radio_usb" -# build_firmware "Heltec_v2_companion_radio_ble" -# build_firmware "Heltec_v3_companion_radio_usb" -# build_firmware "Heltec_v3_companion_radio_ble" -# build_firmware "Xiao_S3_WIO_companion_radio_ble" -# build_firmware "LilyGo_T3S3_sx1262_companion_radio_usb" -# build_firmware "LilyGo_T3S3_sx1262_companion_radio_ble" -# build_firmware "RAK_4631_companion_radio_usb" -# build_firmware "RAK_4631_companion_radio_ble" -# build_firmware "t1000e_companion_radio_ble" - - # build all companion firmwares - build_all_firmwares_by_suffix "_companion_radio_usb" - build_all_firmwares_by_suffix "_companion_radio_ble" - + build_all_firmwares_by_suffix "$BULK_BUILD_SUFFIX_COMPANION_USB" + build_all_firmwares_by_suffix "$BULK_BUILD_SUFFIX_COMPANION_BLE" } build_room_server_firmwares() { - -# # build specific room server firmwares -# build_firmware "Heltec_v3_room_server" -# build_firmware "RAK_4631_room_server" - - # build all room server firmwares - build_all_firmwares_by_suffix "_room_server" - + build_all_firmwares_by_suffix "$BULK_BUILD_SUFFIX_ROOM_SERVER" } build_firmwares() { @@ -245,34 +659,86 @@ build_firmwares() { build_room_server_firmwares } -# clean build dir -rm -rf out -mkdir -p out +prepare_output_dir() { + local output_dir="$OUTPUT_DIR" -# handle script args -if [[ $1 == "build-firmware" ]]; then - TARGETS=${@:2} - if [ "$TARGETS" ]; then - for env in $TARGETS; do - build_firmware $env - done - else - echo "usage: $0 build-firmware " + if [ -z "$output_dir" ] || [ "$output_dir" == "/" ] || [ "$output_dir" == "." ]; then + echo "Refusing to clean unsafe output directory: $output_dir" exit 1 fi -elif [[ $1 == "build-matching-firmwares" ]]; then - if [ "$2" ]; then - build_all_firmwares_matching $2 - else - echo "usage: $0 build-matching-firmwares " + + rm -rf -- "$output_dir" + mkdir -p -- "$output_dir" +} + +run_build_firmware_command() { + local targets=("${@:2}") + local env + + if [ ${#targets[@]} -eq 0 ]; then + echo "usage: $0 build-firmware " exit 1 fi -elif [[ $1 == "build-firmwares" ]]; then - build_firmwares -elif [[ $1 == "build-companion-firmwares" ]]; then - build_companion_firmwares -elif [[ $1 == "build-repeater-firmwares" ]]; then - build_repeater_firmwares -elif [[ $1 == "build-room-server-firmwares" ]]; then - build_room_server_firmwares -fi + + for env in "${targets[@]}"; do + build_firmware "$env" + done +} + +run_command() { + case "$1" in + build-firmware) + run_build_firmware_command "$@" + ;; + build-matching-firmwares) + if [ -n "$2" ]; then + build_all_firmwares_matching "$2" + else + echo "usage: $0 build-matching-firmwares " + exit 1 + fi + ;; + build-firmwares) + build_firmwares + ;; + build-companion-firmwares) + build_companion_firmwares + ;; + build-repeater-firmwares) + build_repeater_firmwares + ;; + build-room-server-firmwares) + build_room_server_firmwares + ;; + *) + global_usage + exit 1 + ;; + esac +} + +main() { + case "${1:-}" in + help|usage|-h|--help) + global_usage + exit 0 + ;; + list|-l) + init_project_context + get_pio_envs + exit 0 + ;; + esac + + init_project_context + + if [ $# -eq 0 ]; then + prompt_for_board_target + set -- build-firmware "$SELECTED_TARGET" + fi + + prepare_output_dir + run_command "$@" +} + +main "$@" From 166f804da169bc3d0301fd3cb5ff5bca291c1ebe Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 20 Apr 2026 13:47:01 -0700 Subject: [PATCH 03/94] Remove unrelated CLI changes from PR branch --- docs/cli_commands.md | 10 ---------- src/helpers/CommonCLI.cpp | 29 ----------------------------- 2 files changed, 39 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 57ab6c6459..0e785f4e83 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -30,16 +30,6 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -### Enter the UF2 bootloader (nRF52 only) -**Usage:** -- `uf2reset` - -**Serial Only:** Yes - -**Note:** Reboots directly into the UF2 bootloader on supported nRF52 boards. - ---- - ### Reset the clock and reboot **Usage:** - `clkreboot` diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index ab035f6623..2f7a0fffcb 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -4,29 +4,6 @@ #include "AdvertDataHelpers.h" #include -#if defined(NRF52_PLATFORM) -#include -#include - -#ifndef DFU_MAGIC_UF2_RESET -#define DFU_MAGIC_UF2_RESET 0x57 -#endif - -static void resetToUf2Bootloader() { - uint8_t sd_enabled = 0; - sd_softdevice_is_enabled(&sd_enabled); - - if (sd_enabled) { - sd_power_gpregret_clr(0, 0xFF); - sd_power_gpregret_set(0, DFU_MAGIC_UF2_RESET); - } else { - NRF_POWER->GPREGRET = DFU_MAGIC_UF2_RESET; - } - - NVIC_SystemReset(); -} -#endif - #ifndef BRIDGE_MAX_BAUD #define BRIDGE_MAX_BAUD 115200 #endif @@ -233,12 +210,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch _board->powerOff(); // doesn't return } else if (memcmp(command, "reboot", 6) == 0) { _board->reboot(); // doesn't return - } else if (sender_timestamp == 0 && memcmp(command, "uf2reset", 8) == 0 && (command[8] == 0 || command[8] == ' ')) { // from serial command line only -#if defined(NRF52_PLATFORM) - resetToUf2Bootloader(); // doesn't return -#else - strcpy(reply, "ERR: unsupported"); -#endif } else if (memcmp(command, "clkreboot", 9) == 0) { // Reset clock getRTCClock()->setCurrentTime(1715770351); // 15 May 2024, 8:50pm From 4c393348dfd9a55e48e2f6bcd77ce70e4e6aa5e9 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 20 Apr 2026 13:53:38 -0700 Subject: [PATCH 04/94] Add new line at the end of the file. --- build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sh b/build.sh index cd45e8d8d8..4b47220d21 100755 --- a/build.sh +++ b/build.sh @@ -742,3 +742,4 @@ main() { } main "$@" + From d8552e33448391d90a0275a013db3b23d8da8b39 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 20 Apr 2026 14:06:39 -0700 Subject: [PATCH 05/94] Better json arg passing in build.sh --- build.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build.sh b/build.sh index 4b47220d21..30b73b5ffa 100755 --- a/build.sh +++ b/build.sh @@ -495,7 +495,8 @@ get_platform_for_env() { # PlatformIO exposes project config as JSON; scan the selected env's # build_flags to recover the platform token used for artifact collection. - echo "$PIO_CONFIG_JSON" | python3 -c " + # Feed the cached JSON via stdin to avoid shell echo quirks and argv/env size limits. + python3 -c " import sys, json, re data = json.load(sys.stdin) for section, options in data: @@ -507,7 +508,7 @@ for section, options in data: if match: print(match.group(1)) sys.exit(0) -" +" <<<"$PIO_CONFIG_JSON" } is_supported_platform() { From 8525b4e980a9eef86cf4cf475f7936b3cd69a101 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 21 Apr 2026 16:24:00 -0700 Subject: [PATCH 06/94] For packets with a path set; auto try again if no echo was heard --- docs/cli_commands.md | 28 +++ examples/simple_repeater/MyMesh.cpp | 71 ++++++++ examples/simple_repeater/MyMesh.h | 7 + src/Dispatcher.cpp | 4 +- src/Dispatcher.h | 2 + src/Mesh.cpp | 271 +++++++++++++++++++++++++++- src/Mesh.h | 41 +++++ src/helpers/CommonCLI.cpp | 51 +++++- src/helpers/CommonCLI.h | 4 +- src/helpers/SimpleMeshTables.h | 172 +++++++++++++++--- 10 files changed, 619 insertions(+), 32 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 0e785f4e83..a5b17af96e 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -467,6 +467,34 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change whether direct retries can fall back to the recently-heard repeater list +**Usage:** +- `get direct.retry.heard` +- `set direct.retry.heard ` + +**Parameters:** +- `state`: `on`|`off` + +**Default:** `off` + +**Note:** When enabled, a repeater can use recently-heard non-duplicate repeater prefixes as a fallback for direct retry eligibility when no suitable neighbor entry is available. + +--- + +#### View or change the SNR margin used for direct retry eligibility +**Usage:** +- `get direct.retry.margin` +- `set direct.retry.margin ` + +**Parameters:** +- `value`: Margin in dB above the SF-specific receive floor (minimum `0`, default `5`) + +**Default:** `5` + +**Note:** The retry gate uses the active SF floor of `SF5=-2.5`, `SF6=-5`, `SF7=-7.5`, `SF8=-10`, `SF9=-12.5`, `SF10=-15`, `SF11=-17.5`, `SF12=-20`, then adds this margin. + +--- + #### [Experimental] View or change the processing delay for received traffic **Usage:** - `get rxdelay` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 24e8894927..33c350e3e2 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -40,6 +40,9 @@ #ifndef TXT_ACK_DELAY #define TXT_ACK_DELAY 200 #endif +#ifndef HALO_DIRECT_RETRY_DELAY_MIN + #define HALO_DIRECT_RETRY_DELAY_MIN 200 +#endif #define FIRMWARE_VER_LEVEL 2 @@ -60,6 +63,20 @@ #define LAZY_CONTACTS_WRITE_DELAY 5000 +const NeighbourInfo* MyMesh::findNeighbourByHash(const uint8_t* hash, uint8_t hash_len) const { +#if MAX_NEIGHBOURS + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + if (neighbours[i].heard_timestamp > 0 && neighbours[i].id.isHashMatch(hash, hash_len)) { + return &neighbours[i]; + } + } +#else + (void)hash; + (void)hash_len; +#endif + return NULL; +} + void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { #if MAX_NEIGHBOURS // check if neighbours enabled // find existing neighbour, else use least recently updated @@ -399,6 +416,8 @@ File MyMesh::openAppend(const char *fname) { static uint8_t max_loop_minimal[] = { 0, /* 1-byte */ 4, /* 2-byte */ 2, /* 3-byte */ 1 }; static uint8_t max_loop_moderate[] = { 0, /* 1-byte */ 2, /* 2-byte */ 1, /* 3-byte */ 1 }; static uint8_t max_loop_strict[] = { 0, /* 1-byte */ 1, /* 2-byte */ 1, /* 3-byte */ 1 }; +// SF5..SF12 receive floors, scaled by 4 so we can keep the retry gate in int8_t quarter-dB units. +static const int8_t direct_retry_floor_x4[] = { -10, -20, -30, -40, -50, -60, -70, -80 }; bool MyMesh::isLooped(const mesh::Packet* packet, const uint8_t max_counters[]) { uint8_t hash_size = packet->getPathHashSize(); @@ -531,6 +550,44 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } +int8_t MyMesh::getDirectRetryMinSNRX4() const { + // Use the live SF so `tempradio` changes immediately affect the retry threshold. + uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); + int16_t threshold = direct_retry_floor_x4[sf - 5] + ((int16_t)_prefs.direct_retry_snr_margin_db * 4); + return (int8_t)constrain(threshold, -128, 127); +} +bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { + if (_prefs.disable_fwd) { + return false; + } + + int8_t min_snr_x4 = getDirectRetryMinSNRX4(); + const NeighbourInfo* neighbour = findNeighbourByHash(next_hop_hash, next_hop_hash_len); + // Prefer the explicit neighbor table first; it is the strongest signal that this hop is still reachable. + if (neighbour != NULL && neighbour->snr >= min_snr_x4) { + return true; + } + + if (!_prefs.direct_retry_recent_enabled) { + return false; + } + + // If no neighbor entry exists, fall back to the recent-heard repeater cache keyed by the same path prefix. + const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len); + return recent != NULL && recent->snr_x4 >= min_snr_x4; +} +uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { + // Approximate LoRa line rate in kilobits/sec from the live radio params the repeater is using now. + float kbps = (((float) active_sf) * active_bw * ((float) active_cr)) / ((float) (1UL << active_sf)); + if (kbps <= 0.0f) { + return HALO_DIRECT_RETRY_DELAY_MIN; + } + + // Wait roughly long enough for our transmission, the next hop's receive/forward window, and its echo back. + uint32_t bits = ((uint32_t) packet->getRawLength()) * 8; + uint32_t scaled_wait_millis = (uint32_t) ((((float) bits) * 4.0f) / kbps); + return max((uint32_t) HALO_DIRECT_RETRY_DELAY_MIN, scaled_wait_millis); +} bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { // just try to determine region for packet (apply later in allowPacketForward()) @@ -859,6 +916,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.tx_delay_factor = 0.5f; // was 0.25f _prefs.direct_tx_delay_factor = 0.3f; // was 0.2 + _prefs.direct_retry_recent_enabled = 0; + _prefs.direct_retry_snr_margin_db = 5; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; _prefs.node_lon = ADVERT_LON; @@ -899,6 +958,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc pending_discover_tag = 0; pending_discover_until = 0; + active_bw = _prefs.bw; + active_sf = _prefs.sf; + active_cr = _prefs.cr; } void MyMesh::begin(FILESYSTEM *fs) { @@ -917,6 +979,9 @@ void MyMesh::begin(FILESYSTEM *fs) { #endif radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + active_bw = _prefs.bw; + active_sf = _prefs.sf; + active_cr = _prefs.cr; radio_set_tx_power(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); @@ -1314,12 +1379,18 @@ void MyMesh::loop() { if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params set_radio_at = 0; // clear timer radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr); + active_bw = pending_bw; + active_sf = pending_sf; + active_cr = pending_cr; MESH_DEBUG_PRINTLN("Temp radio params"); } if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig revert_radio_at = 0; // clear timer radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + active_bw = _prefs.bw; + active_sf = _prefs.sf; + active_cr = _prefs.cr; MESH_DEBUG_PRINTLN("Radio params restored"); } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 929584484f..2d7d8dc1fb 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -109,8 +109,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { unsigned long set_radio_at, revert_radio_at; float pending_freq; float pending_bw; + float active_bw; // live BW, including temporary radio overrides uint8_t pending_sf; + uint8_t active_sf; // live SF, including temporary radio overrides uint8_t pending_cr; + uint8_t active_cr; // live CR, including temporary radio overrides int matching_peer_indexes[MAX_CLIENTS]; #if defined(WITH_RS232_BRIDGE) RS232Bridge bridge; @@ -118,6 +121,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { ESPNowBridge bridge; #endif + const NeighbourInfo* findNeighbourByHash(const uint8_t* hash, uint8_t hash_len) const; + int8_t getDirectRetryMinSNRX4() const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); void sendNodeDiscoverReq(); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); @@ -146,6 +151,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t getRetransmitDelay(const mesh::Packet* packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; + bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override; + uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override; int getInterferenceThreshold() const override { return _prefs.interference_threshold; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 9d7a11131d..cccbd36c79 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -106,6 +106,7 @@ void Dispatcher::loop() { _radio->onSendFinished(); logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); + onSendComplete(outbound); if (outbound->isRouteFlood()) { n_sent_flood++; } else { @@ -118,6 +119,7 @@ void Dispatcher::loop() { _radio->onSendFinished(); logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); + onSendFail(outbound); releasePacket(outbound); // return to pool outbound = NULL; @@ -386,4 +388,4 @@ unsigned long Dispatcher::futureMillis(int millis_from_now) const { return _ms->getMillis() + millis_from_now; } -} \ No newline at end of file +} diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 2a99d0682b..163c61963e 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -159,6 +159,8 @@ class Dispatcher { virtual void logRx(Packet* packet, int len, float score) { } // hooks for custom logging virtual void logTx(Packet* packet, int len) { } virtual void logTxFail(Packet* packet, int len) { } + virtual void onSendComplete(Packet* packet) { } + virtual void onSendFail(Packet* packet) { } virtual const char* getLogDateTime() { return ""; } virtual float getAirtimeBudgetFactor() const; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 57fee14036..b9b39c952e 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -4,11 +4,32 @@ namespace mesh { void Mesh::begin() { + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + _direct_retries[i].packet = NULL; + _direct_retries[i].trigger_packet = NULL; + _direct_retries[i].retry_at = 0; + _direct_retries[i].retry_delay = 0; + _direct_retries[i].priority = 0; + _direct_retries[i].progress_marker = 0; + _direct_retries[i].expect_path_growth = false; + _direct_retries[i].queued = false; + _direct_retries[i].active = false; + } Dispatcher::begin(); } void Mesh::loop() { Dispatcher::loop(); + + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active || !_direct_retries[i].queued || !millisHasNowPassed(_direct_retries[i].retry_at)) { + continue; + } + + if (!isDirectRetryQueued(_direct_retries[i].packet)) { + clearDirectRetrySlot(i); + } + } } bool Mesh::allowPacketForward(const mesh::Packet* packet) { @@ -22,10 +43,25 @@ uint32_t Mesh::getRetransmitDelay(const mesh::Packet* packet) { uint32_t Mesh::getDirectRetransmitDelay(const Packet* packet) { return 0; // by default, no delay } +bool Mesh::allowDirectRetry(const Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { + return false; +} +uint32_t Mesh::getDirectRetryEchoDelay(const Packet* packet) const { + // Keep the base fallback aligned with the repeater's minimum retry wait. + return 200; +} uint8_t Mesh::getExtraAckTransmitCount() const { return 0; } +void Mesh::onSendComplete(Packet* packet) { + armDirectRetryOnSendComplete(packet); +} + +void Mesh::onSendFail(Packet* packet) { + clearPendingDirectRetryOnSendFail(packet); +} + uint32_t Mesh::getCADFailRetryDelay() const { return _rng->nextInt(1, 4)*120; } @@ -39,6 +75,10 @@ int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int } DispatcherAction Mesh::onRecvPacket(Packet* pkt) { + if (pkt->isRouteDirect()) { + cancelDirectRetryOnEcho(pkt); + } + if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { if (pkt->path_len < MAX_PATH_SIZE) { uint8_t i = 0; @@ -58,6 +98,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { pkt->path[pkt->path_len++] = (int8_t) (pkt->getSNR()*4); uint32_t d = getDirectRetransmitDelay(pkt); + maybeScheduleDirectRetry(pkt, 5); return ACTION_RETRANSMIT_DELAYED(5, d); // schedule with priority 5 (for now), maybe make configurable? } } @@ -98,6 +139,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { removeSelfFromPath(pkt); uint32_t d = getDirectRetransmitDelay(pkt); + maybeScheduleDirectRetry(pkt, 0); return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority } } @@ -372,6 +414,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len); a1->header &= ~PH_ROUTE_MASK; a1->header |= ROUTE_TYPE_DIRECT; + maybeScheduleDirectRetry(a1, 0); sendPacket(a1, 0, delay_millis); } extra--; @@ -382,11 +425,225 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len); a2->header &= ~PH_ROUTE_MASK; a2->header |= ROUTE_TYPE_DIRECT; + maybeScheduleDirectRetry(a2, 0); sendPacket(a2, 0, delay_millis); } } } +void Mesh::clearDirectRetrySlot(int idx) { + _direct_retries[idx].packet = NULL; + _direct_retries[idx].trigger_packet = NULL; + _direct_retries[idx].retry_at = 0; + _direct_retries[idx].retry_delay = 0; + _direct_retries[idx].priority = 0; + _direct_retries[idx].progress_marker = 0; + _direct_retries[idx].expect_path_growth = false; + _direct_retries[idx].queued = false; + _direct_retries[idx].active = false; +} + +bool Mesh::isDirectRetryQueued(const Packet* packet) const { + for (int i = 0; i < _mgr->getOutboundTotal(); i++) { + if (_mgr->getOutboundByIdx(i) == packet) { + return true; + } + } + return false; +} + +void Mesh::calculateDirectRetryKey(const Packet* packet, uint8_t* dest_key) const { + uint8_t type = packet->getPayloadType(); + Utils::sha256(dest_key, MAX_HASH_SIZE, &type, 1, packet->payload, packet->payload_len); +} + +bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { + uint8_t recv_key[MAX_HASH_SIZE]; + calculateDirectRetryKey(packet, recv_key); + + bool cleared = false; + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active || memcmp(recv_key, _direct_retries[i].retry_key, MAX_HASH_SIZE) != 0) { + continue; + } + + bool is_echo = _direct_retries[i].expect_path_growth + ? packet->path_len > _direct_retries[i].progress_marker + : packet->getPathHashCount() < _direct_retries[i].progress_marker; + if (!is_echo) { + continue; + } + + if (_direct_retries[i].queued) { + for (int j = 0; j < _mgr->getOutboundTotal(); j++) { + if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { + Packet* pending = _mgr->removeOutboundByIdx(j); + if (pending) { + releasePacket(pending); + } + break; + } + } + clearDirectRetrySlot(i); + } else { + clearDirectRetrySlot(i); + } + cleared = true; + } + + return cleared; +} + +void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active) { + continue; + } + + if (_direct_retries[i].queued) { + if (_direct_retries[i].packet == packet) { + // The retry packet itself just finished transmitting; Dispatcher will release it after this hook. + clearDirectRetrySlot(i); + } + continue; + } + + if (_direct_retries[i].trigger_packet != packet) { + continue; + } + + // Allocate the retry packet only after TX-complete so busy repeaters do not reserve pool slots early. + Packet* retry = obtainNewPacket(); + if (retry == NULL) { + clearDirectRetrySlot(i); + continue; + } + + *retry = *packet; + + // Start the echo wait only after the initial direct transmission actually completed. + sendPacket(retry, _direct_retries[i].priority, _direct_retries[i].retry_delay); + if (isDirectRetryQueued(retry)) { + _direct_retries[i].packet = retry; + _direct_retries[i].trigger_packet = NULL; + _direct_retries[i].queued = true; + _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); + } else { + clearDirectRetrySlot(i); + } + } +} + +void Mesh::clearPendingDirectRetryOnSendFail(const Packet* packet) { + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active) { + continue; + } + + if (_direct_retries[i].queued) { + if (_direct_retries[i].packet == packet) { + // The queued retry itself failed; Dispatcher will release it after this hook. + clearDirectRetrySlot(i); + } + continue; + } + + if (_direct_retries[i].trigger_packet == packet) { + clearDirectRetrySlot(i); + } + } +} + +bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len, + uint8_t& progress_marker, bool& expect_path_growth) const { + switch (packet->getPayloadType()) { + case PAYLOAD_TYPE_ACK: + case PAYLOAD_TYPE_PATH: + case PAYLOAD_TYPE_REQ: + case PAYLOAD_TYPE_RESPONSE: + case PAYLOAD_TYPE_TXT_MSG: + case PAYLOAD_TYPE_ANON_REQ: + if (packet->getPathHashCount() <= 1) { + return false; + } + next_hop_hash = packet->path; + next_hop_hash_len = packet->getPathHashSize(); + progress_marker = packet->getPathHashCount(); + expect_path_growth = false; + return true; + + case PAYLOAD_TYPE_MULTIPART: + if (packet->payload_len < 1 || (packet->payload[0] & 0x0F) != PAYLOAD_TYPE_ACK || packet->getPathHashCount() <= 1) { + return false; + } + next_hop_hash = packet->path; + next_hop_hash_len = packet->getPathHashSize(); + progress_marker = packet->getPathHashCount(); + expect_path_growth = false; + return true; + + case PAYLOAD_TYPE_TRACE: { + if (packet->payload_len < 9) { + return false; + } + + uint8_t hash_size = 1 << (packet->payload[8] & 0x03); + uint8_t route_bytes = packet->payload_len - 9; + uint8_t offset = packet->path_len * hash_size; + if (offset + hash_size > route_bytes) { + return false; + } + if (offset + (2 * hash_size) > route_bytes) { + return false; // no downstream repeater means there will be no forward echo to overhear. + } + + next_hop_hash = &packet->payload[9 + offset]; + next_hop_hash_len = hash_size; + progress_marker = packet->path_len; + expect_path_growth = true; + return true; + } + + default: + return false; + } +} + +void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { + const uint8_t* next_hop_hash; + uint8_t next_hop_hash_len; + uint8_t progress_marker; + bool expect_path_growth; + if (!getDirectRetryTarget(packet, next_hop_hash, next_hop_hash_len, progress_marker, expect_path_growth) + || !allowDirectRetry(packet, next_hop_hash, next_hop_hash_len)) { + return; + } + + int slot_idx = -1; + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active) { + slot_idx = i; + break; + } + } + if (slot_idx < 0) { + return; + } + + // Only store retry metadata here; allocate the retry packet after the initial TX really completes. + uint32_t retry_delay = getDirectRetryEchoDelay(packet); + calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key); + _direct_retries[slot_idx].packet = NULL; + _direct_retries[slot_idx].trigger_packet = const_cast(packet); + _direct_retries[slot_idx].retry_at = 0; + _direct_retries[slot_idx].retry_delay = retry_delay; + _direct_retries[slot_idx].priority = priority; + _direct_retries[slot_idx].progress_marker = progress_marker; + _direct_retries[slot_idx].expect_path_growth = expect_path_growth; + _direct_retries[slot_idx].queued = false; + _direct_retries[slot_idx].active = true; +} + Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) { if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL; @@ -634,7 +891,7 @@ void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_si packet->header |= ROUTE_TYPE_FLOOD; packet->setPathHashSizeAndCount(path_hash_size, 0); - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us uint8_t pri; if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { @@ -663,7 +920,7 @@ void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_m packet->transport_codes[1] = transport_codes[1]; packet->setPathHashSizeAndCount(path_hash_size, 0); - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us uint8_t pri; if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { @@ -696,7 +953,8 @@ void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uin pri = 0; } } - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us + maybeScheduleDirectRetry(packet, pri); sendPacket(packet, pri, delay_millis); } @@ -706,7 +964,7 @@ void Mesh::sendZeroHop(Packet* packet, uint32_t delay_millis) { packet->path_len = 0; // path_len of zero means Zero Hop - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us sendPacket(packet, 0, delay_millis); } @@ -719,9 +977,10 @@ void Mesh::sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay packet->path_len = 0; // path_len of zero means Zero Hop - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us sendPacket(packet, 0, delay_millis); } -} \ No newline at end of file +} + diff --git a/src/Mesh.h b/src/Mesh.h index f9f8786320..94bfec9f1b 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -4,6 +4,10 @@ namespace mesh { +#ifndef MAX_DIRECT_RETRY_SLOTS + #define MAX_DIRECT_RETRY_SLOTS 6 +#endif + class GroupChannel { public: uint8_t hash[PATH_HASH_SIZE]; @@ -16,6 +20,7 @@ class GroupChannel { class MeshTables { public: virtual bool hasSeen(const Packet* packet) = 0; + virtual void markSent(const Packet* packet) = 0; virtual void clear(const Packet* packet) = 0; // remove this packet hash from table }; @@ -24,17 +29,42 @@ class MeshTables { * and provides virtual methods for sub-classes on handling incoming, and also preparing outbound Packets. */ class Mesh : public Dispatcher { + struct DirectRetryEntry { + Packet* packet; + Packet* trigger_packet; + unsigned long retry_at; + uint32_t retry_delay; + uint8_t retry_key[MAX_HASH_SIZE]; + uint8_t priority; + uint8_t progress_marker; + bool expect_path_growth; + bool queued; + bool active; + }; + RTCClock* _rtc; RNG* _rng; MeshTables* _tables; + DirectRetryEntry _direct_retries[MAX_DIRECT_RETRY_SLOTS]; void removeSelfFromPath(Packet* packet); void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis); + void clearDirectRetrySlot(int idx); + bool isDirectRetryQueued(const Packet* packet) const; + void calculateDirectRetryKey(const Packet* packet, uint8_t* dest_key) const; + bool cancelDirectRetryOnEcho(const Packet* packet); + void armDirectRetryOnSendComplete(const Packet* packet); + void clearPendingDirectRetryOnSendFail(const Packet* packet); + bool getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len, + uint8_t& progress_marker, bool& expect_path_growth) const; + void maybeScheduleDirectRetry(const Packet* packet, uint8_t priority); //void routeRecvAcks(Packet* packet, uint32_t delay_millis); DispatcherAction forwardMultipartDirect(Packet* pkt); protected: DispatcherAction onRecvPacket(Packet* pkt) override; + void onSendComplete(Packet* packet) override; + void onSendFail(Packet* packet) override; virtual uint32_t getCADFailRetryDelay() const override; @@ -65,6 +95,17 @@ class Mesh : public Dispatcher { */ virtual uint32_t getDirectRetransmitDelay(const Packet* packet); + /** + * \brief Decide whether a DIRECT packet should get one delayed retry if the next hop echo is not overheard. + * Sub-classes can use neighbour tables or other link-quality data to opt in selectively. + */ + virtual bool allowDirectRetry(const Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const; + + /** + * \returns milliseconds to wait for the next-hop echo before queueing one retry of the DIRECT packet. + */ + virtual uint32_t getDirectRetryEchoDelay(const Packet* packet) const; + /** * \returns number of extra (Direct) ACK transmissions wanted. */ diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 2f7a0fffcb..c7095b2640 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -8,6 +8,13 @@ #define BRIDGE_MAX_BAUD 115200 #endif +// These bytes used to be reserved/unused in persisted prefs, so keep a marker before trusting them. +#define DIRECT_RETRY_PREFS_MAGIC_0 0xD4 +#define DIRECT_RETRY_PREFS_MAGIC_1 0x52 +#define DIRECT_RETRY_RECENT_DEFAULT 0 +#define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT 5 +#define DIRECT_RETRY_SNR_MARGIN_DB_MAX 40 + // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -60,7 +67,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84 file.read((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88 file.read((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104 - file.read(pad, 4); // 108 : 4 bytes unused + file.read((uint8_t *)&_prefs->direct_retry_recent_enabled, sizeof(_prefs->direct_retry_recent_enabled)); // 108 + file.read((uint8_t *)&_prefs->direct_retry_snr_margin_db, sizeof(_prefs->direct_retry_snr_margin_db)); // 109 + file.read((uint8_t *)&_prefs->direct_retry_prefs_magic[0], sizeof(_prefs->direct_retry_prefs_magic)); // 110 file.read((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112 file.read((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113 file.read((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 @@ -102,6 +111,15 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1); _prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 10.0f); _prefs->path_hash_mode = constrain(_prefs->path_hash_mode, 0, 2); // NOTE: mode 3 reserved for future + // Old firmware left offset 108..111 undefined, so require the marker before using the new retry prefs. + if (_prefs->direct_retry_prefs_magic[0] != DIRECT_RETRY_PREFS_MAGIC_0 + || _prefs->direct_retry_prefs_magic[1] != DIRECT_RETRY_PREFS_MAGIC_1) { + _prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT; + _prefs->direct_retry_snr_margin_db = DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT; + } else { + _prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1); + _prefs->direct_retry_snr_margin_db = constrain(_prefs->direct_retry_snr_margin_db, 0, DIRECT_RETRY_SNR_MARGIN_DB_MAX); + } // sanitise bad bridge pref values _prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1); @@ -150,7 +168,11 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84 file.write((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88 file.write((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104 - file.write(pad, 4); // 108 : 4 byte unused + file.write((uint8_t *)&_prefs->direct_retry_recent_enabled, sizeof(_prefs->direct_retry_recent_enabled)); // 108 + file.write((uint8_t *)&_prefs->direct_retry_snr_margin_db, sizeof(_prefs->direct_retry_snr_margin_db)); // 109 + // Persist a marker so later loads can distinguish real values from legacy garbage in this reserved slot. + uint8_t retry_magic[2] = { DIRECT_RETRY_PREFS_MAGIC_0, DIRECT_RETRY_PREFS_MAGIC_1 }; + file.write(retry_magic, sizeof(retry_magic)); // 110 file.write((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112 file.write((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113 file.write((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 @@ -338,6 +360,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); } else if (memcmp(config, "direct.txdelay", 14) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); + } else if (memcmp(config, "direct.retry.heard", 18) == 0) { + sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); + } else if (memcmp(config, "direct.retry.margin", 19) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_snr_margin_db); } else if (memcmp(config, "owner.info", 10) == 0) { *reply++ = '>'; *reply++ = ' '; @@ -587,6 +613,27 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else { strcpy(reply, "Error, cannot be negative"); } + } else if (memcmp(config, "direct.retry.heard ", 19) == 0) { + if (memcmp(&config[19], "on", 2) == 0) { + _prefs->direct_retry_recent_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(&config[19], "off", 3) == 0) { + _prefs->direct_retry_recent_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be on or off"); + } + } else if (memcmp(config, "direct.retry.margin ", 20) == 0) { + int db = atoi(&config[20]); + if (db >= 0 && db <= DIRECT_RETRY_SNR_MARGIN_DB_MAX) { + _prefs->direct_retry_snr_margin_db = (uint8_t)db; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min 0 and max %d", DIRECT_RETRY_SNR_MARGIN_DB_MAX); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 3a4332d1f2..c1e0c5e9ed 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -32,7 +32,9 @@ struct NodePrefs { // persisted to file float tx_delay_factor; char guest_password[16]; float direct_tx_delay_factor; - uint32_t guard; + uint8_t direct_retry_recent_enabled; + uint8_t direct_retry_snr_margin_db; + uint8_t direct_retry_prefs_magic[2]; uint8_t sf; uint8_t cr; uint8_t allow_read_only; diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 2f8af52af1..217fd5a08c 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -8,13 +8,103 @@ #define MAX_PACKET_HASHES 128 #define MAX_PACKET_ACKS 64 +#define MAX_RECENT_REPEATERS 64 +#define MAX_ROUTE_HASH_BYTES 3 class SimpleMeshTables : public mesh::MeshTables { +public: + struct RecentRepeaterInfo { + // Just enough identity to match a next-hop path prefix plus the SNR that heard it. + uint8_t prefix[MAX_ROUTE_HASH_BYTES]; + uint8_t prefix_len; + int8_t snr_x4; + }; + +private: uint8_t _hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE]; int _next_idx; uint32_t _acks[MAX_PACKET_ACKS]; int _next_ack_idx; uint32_t _direct_dups, _flood_dups; + RecentRepeaterInfo _recent_repeaters[MAX_RECENT_REPEATERS]; + int _next_recent_repeater_idx; + + bool hasSeenAck(uint32_t ack) const { + for (int i = 0; i < MAX_PACKET_ACKS; i++) { + if (ack == _acks[i]) { + return true; + } + } + return false; + } + + void storeAck(uint32_t ack) { + _acks[_next_ack_idx] = ack; + _next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS; + } + + bool hasSeenHash(const uint8_t* hash) const { + const uint8_t* sp = _hashes; + for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { + if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { + return true; + } + } + return false; + } + + void storeHash(const uint8_t* hash) { + memcpy(&_hashes[_next_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE); + _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; + } + + bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { + // Learn repeater prefixes only from packet shapes that expose a trustworthy repeater ID. + if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->payload_len >= PUB_KEY_SIZE) { + memcpy(prefix, packet->payload, MAX_ROUTE_HASH_BYTES); + prefix_len = MAX_ROUTE_HASH_BYTES; + return true; + } + + if (packet->getPayloadType() == PAYLOAD_TYPE_CONTROL + && packet->isRouteDirect() + && packet->getPathHashCount() == 0 + && packet->payload_len >= 6 + MAX_ROUTE_HASH_BYTES + && (packet->payload[0] & 0xF0) == 0x90) { + memcpy(prefix, &packet->payload[6], MAX_ROUTE_HASH_BYTES); + prefix_len = MAX_ROUTE_HASH_BYTES; + return true; + } + + if (packet->isRouteFlood() && packet->getPathHashCount() > 0) { + prefix_len = packet->getPathHashSize(); + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + const uint8_t* last_hop = &packet->path[(packet->getPathHashCount() - 1) * packet->getPathHashSize()]; + memcpy(prefix, last_hop, prefix_len); + return true; + } + + return false; + } + + void recordRecentRepeater(const mesh::Packet* packet) { + uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; + uint8_t prefix_len = 0; + if (!extractRecentRepeater(packet, prefix, prefix_len) || prefix_len == 0) { + return; + } + + // Ring buffer is enough here; retry fallback only needs a recent prefix->SNR observation. + RecentRepeaterInfo& slot = _recent_repeaters[_next_recent_repeater_idx]; + memset(slot.prefix, 0, sizeof(slot.prefix)); + memcpy(slot.prefix, prefix, prefix_len); + slot.prefix_len = prefix_len; + slot.snr_x4 = packet->_snr; + _next_recent_repeater_idx = (_next_recent_repeater_idx + 1) % MAX_RECENT_REPEATERS; + } public: SimpleMeshTables() { @@ -23,6 +113,8 @@ class SimpleMeshTables : public mesh::MeshTables { memset(_acks, 0, sizeof(_acks)); _next_ack_idx = 0; _direct_dups = _flood_dups = 0; + memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); + _next_recent_repeater_idx = 0; } #ifdef ESP32 @@ -31,12 +123,16 @@ class SimpleMeshTables : public mesh::MeshTables { f.read((uint8_t *) &_next_idx, sizeof(_next_idx)); f.read((uint8_t *) &_acks[0], sizeof(_acks)); f.read((uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx)); + f.read((uint8_t *) &_recent_repeaters[0], sizeof(_recent_repeaters)); + f.read((uint8_t *) &_next_recent_repeater_idx, sizeof(_next_recent_repeater_idx)); } void saveTo(File f) { f.write(_hashes, sizeof(_hashes)); f.write((const uint8_t *) &_next_idx, sizeof(_next_idx)); f.write((const uint8_t *) &_acks[0], sizeof(_acks)); f.write((const uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx)); + f.write((const uint8_t *) &_recent_repeaters[0], sizeof(_recent_repeaters)); + f.write((const uint8_t *) &_next_recent_repeater_idx, sizeof(_next_recent_repeater_idx)); } #endif @@ -44,42 +140,55 @@ class SimpleMeshTables : public mesh::MeshTables { if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { uint32_t ack; memcpy(&ack, packet->payload, 4); - for (int i = 0; i < MAX_PACKET_ACKS; i++) { - if (ack == _acks[i]) { - if (packet->isRouteDirect()) { - _direct_dups++; // keep some stats - } else { - _flood_dups++; - } - return true; + + if (hasSeenAck(ack)) { + if (packet->isRouteDirect()) { + _direct_dups++; // keep some stats + } else { + _flood_dups++; } + return true; } - - _acks[_next_ack_idx] = ack; - _next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS; // cyclic table + + storeAck(ack); return false; } uint8_t hash[MAX_HASH_SIZE]; packet->calculatePacketHash(hash); - const uint8_t* sp = _hashes; - for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { - if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { - if (packet->isRouteDirect()) { - _direct_dups++; // keep some stats - } else { - _flood_dups++; - } - return true; + if (hasSeenHash(hash)) { + if (packet->isRouteDirect()) { + _direct_dups++; // keep some stats + } else { + _flood_dups++; } + return true; } - memcpy(&_hashes[_next_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE); - _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; // cyclic table + storeHash(hash); + recordRecentRepeater(packet); return false; } + void markSent(const mesh::Packet* packet) override { + // Outbound packets must be marked as already-sent without teaching the recent-heard cache about ourselves. + if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { + uint32_t ack; + memcpy(&ack, packet->payload, 4); + if (!hasSeenAck(ack)) { + storeAck(ack); + } + return; + } + + uint8_t hash[MAX_HASH_SIZE]; + packet->calculatePacketHash(hash); + if (!hasSeenHash(hash)) { + storeHash(hash); + } + } + void clear(const mesh::Packet* packet) override { if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { uint32_t ack; @@ -107,5 +216,24 @@ class SimpleMeshTables : public mesh::MeshTables { uint32_t getNumDirectDups() const { return _direct_dups; } uint32_t getNumFloodDups() const { return _flood_dups; } + const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t* hash, uint8_t hash_len) const { + if (hash == NULL || hash_len == 0) { + return NULL; + } + + // Search newest-to-oldest so the retry gate prefers the freshest SNR sample for a prefix. + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; + const RecentRepeaterInfo* info = &_recent_repeaters[idx]; + if (info->prefix_len < hash_len || info->prefix_len == 0) { + continue; + } + if (memcmp(info->prefix, hash, hash_len) == 0) { + return info; + } + } + return NULL; + } + void resetStats() { _direct_dups = _flood_dups = 0; } }; From 577433ce476745945ae7568c6708716b338303da Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 23 Apr 2026 16:25:51 -0700 Subject: [PATCH 07/94] Retry 3 times with a 200ms,300ms,400ms backoff. --- docs/cli_commands.md | 13 +++++ examples/simple_repeater/MyMesh.cpp | 85 ++++++++++++++++++++++++++++ examples/simple_repeater/MyMesh.h | 2 + src/Dispatcher.h | 1 + src/Mesh.cpp | 56 ++++++++++++++++++- src/Mesh.h | 6 ++ src/helpers/SimpleMeshTables.h | 86 +++++++++++++++++++++++++---- 7 files changed, 235 insertions(+), 14 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 3e79019f94..774066b0a1 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -115,6 +115,19 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +### Get or set recent repeater fallback prefix/SNR +**Usage:** +- `recent.repeater` +- `recent.repeater ` + +**Parameters:** +- `prefix_hex`: 1-3 bytes of next-hop prefix (hex) +- `snr_db`: SNR in dB (supports decimals; stored at x4 precision) + +**Note:** `set` is rejected when the prefix already exists in neighbors. + +--- + ## Statistics ### Clear Stats diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 7966404dea..71b532ed86 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -77,6 +77,15 @@ const NeighbourInfo* MyMesh::findNeighbourByHash(const uint8_t* hash, uint8_t ha return NULL; } +bool MyMesh::allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefix_len, void* ctx) { + if (ctx == NULL || prefix == NULL || prefix_len == 0) { + return true; + } + + const MyMesh* self = (const MyMesh*) ctx; + return self->findNeighbourByHash(prefix, prefix_len) == NULL; +} + void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { #if MAX_NEIGHBOURS // check if neighbours enabled // find existing neighbour, else use least recently updated @@ -550,6 +559,34 @@ void MyMesh::logTxFail(mesh::Packet *pkt, int len) { } } +void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis) { + if (packet == NULL) { + return; + } + + MESH_DEBUG_PRINTLN("%s direct retry %s (type=%d, route=%s, payload_len=%d, delay=%lu)", + getLogDateTime(), + event, + (uint32_t)packet->getPayloadType(), + packet->isRouteDirect() ? "D" : "F", + (uint32_t)packet->payload_len, + (unsigned long)delay_millis); + + if (_logging) { + File f = openAppend(PACKET_LOG_FILE); + if (f) { + f.print(getLogDateTime()); + f.printf(": DIRECT RETRY %s (type=%d, route=%s, payload_len=%d, delay=%lu)\n", + event, + (uint32_t)packet->getPayloadType(), + packet->isRouteDirect() ? "D" : "F", + (uint32_t)packet->payload_len, + (unsigned long)delay_millis); + f.close(); + } + } +} + int MyMesh::calcRxDelay(float score, uint32_t air_time) const { if (_prefs.rx_delay_base <= 0.0f) return 0; return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); @@ -976,6 +1013,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc active_bw = _prefs.bw; active_sf = _prefs.sf; active_cr = _prefs.cr; + + ((SimpleMeshTables *)getTables())->setRecentRepeaterAllowFilter(&MyMesh::allowRecentRepeaterPrefixStore, this); } void MyMesh::begin(FILESYSTEM *fs) { @@ -1017,6 +1056,7 @@ void MyMesh::begin(FILESYSTEM *fs) { active_bw = _prefs.bw; active_sf = _prefs.sf; active_cr = _prefs.cr; + ((SimpleMeshTables *)getTables())->setRecentRepeaterMinSNRX4(getDirectRetryMinSNRX4()); radio_set_tx_power(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); @@ -1305,6 +1345,48 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply Serial.printf("\n"); } reply[0] = 0; + } else if (memcmp(command, "recent.repeater", 15) == 0) { + const char* sub = command + 15; + while (*sub == ' ') sub++; + auto* tables = (SimpleMeshTables*)getTables(); + if (*sub == 0) { + const auto* info = tables->getLatestRecentRepeater(); + if (info == NULL) { + strcpy(reply, "> none"); + } else { + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; + mesh::Utils::toHex(hex, info->prefix, info->prefix_len); + sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + } + } else { + char* params = (char*) sub; + char* arg_snr = strchr(params, ' '); + if (arg_snr == NULL) { + strcpy(reply, "Err - usage: recent.repeater "); + } else { + *arg_snr++ = 0; + while (*arg_snr == ' ') arg_snr++; + if (*arg_snr == 0) { + strcpy(reply, "Err - usage: recent.repeater "); + } else { + int hex_len = strlen(params); + int prefix_len = hex_len / 2; + uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; + if ((hex_len % 2) != 0 || prefix_len <= 0 || prefix_len > MAX_ROUTE_HASH_BYTES || !mesh::Utils::fromHex(prefix, prefix_len, params)) { + strcpy(reply, "Err - prefix must be 1-3 bytes hex"); + } else { + float snr_db = strtof(arg_snr, nullptr); + int snr_x4 = (int)(snr_db * 4.0f + (snr_db >= 0.0f ? 0.5f : -0.5f)); + snr_x4 = constrain(snr_x4, -128, 127); + if (tables->setRecentRepeater(prefix, (uint8_t)prefix_len, (int8_t)snr_x4)) { + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - prefix is already in neighbors"); + } + } + } + } + } } else if (memcmp(command, "discover.neighbors", 18) == 0) { const char* sub = command + 18; while (*sub == ' ') sub++; @@ -1358,6 +1440,9 @@ void MyMesh::loop() { MESH_DEBUG_PRINTLN("Radio params restored"); } + // Keep recent-prefix learning aligned with the live retry SNR gate. + ((SimpleMeshTables *)getTables())->setRecentRepeaterMinSNRX4(getDirectRetryMinSNRX4()); + // is pending dirty contacts write needed? if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { acl.save(_fs); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index d2c84b1a9f..16566dca25 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -123,6 +123,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { #endif const NeighbourInfo* findNeighbourByHash(const uint8_t* hash, uint8_t hash_len) const; + static bool allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefix_len, void* ctx); int8_t getDirectRetryMinSNRX4() const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); @@ -153,6 +154,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override; uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override; + void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis) override; int getInterferenceThreshold() const override { return _prefs.interference_threshold; diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 163c61963e..90ee5cdbea 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -170,6 +170,7 @@ class Dispatcher { virtual int getInterferenceThreshold() const { return 0; } // disabled by default virtual int getAGCResetInterval() const { return 0; } // disabled by default virtual unsigned long getDutyCycleWindowMs() const { return 3600000; } + const Packet* getOutboundInFlight() const { return outbound; } public: void begin(); diff --git a/src/Mesh.cpp b/src/Mesh.cpp index b9b39c952e..b9892eedd3 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -3,12 +3,16 @@ namespace mesh { +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS = 3; +static const uint32_t DIRECT_RETRY_BACKOFF_MS[DIRECT_RETRY_MAX_ATTEMPTS] = { 200, 300, 400 }; + void Mesh::begin() { for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { _direct_retries[i].packet = NULL; _direct_retries[i].trigger_packet = NULL; _direct_retries[i].retry_at = 0; _direct_retries[i].retry_delay = 0; + _direct_retries[i].retry_attempts_sent = 0; _direct_retries[i].priority = 0; _direct_retries[i].progress_marker = 0; _direct_retries[i].expect_path_growth = false; @@ -27,6 +31,9 @@ void Mesh::loop() { } if (!isDirectRetryQueued(_direct_retries[i].packet)) { + if (_direct_retries[i].packet == getOutboundInFlight()) { + continue; // currently transmitting; keep slot until onSendComplete/onSendFail emits event + } clearDirectRetrySlot(i); } } @@ -436,6 +443,7 @@ void Mesh::clearDirectRetrySlot(int idx) { _direct_retries[idx].trigger_packet = NULL; _direct_retries[idx].retry_at = 0; _direct_retries[idx].retry_delay = 0; + _direct_retries[idx].retry_attempts_sent = 0; _direct_retries[idx].priority = 0; _direct_retries[idx].progress_marker = 0; _direct_retries[idx].expect_path_growth = false; @@ -484,8 +492,12 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { break; } } + onDirectRetryEvent("canceled_echo", _direct_retries[i].packet, 0); + onDirectRetryEvent("good", _direct_retries[i].packet, 0); clearDirectRetrySlot(i); } else { + onDirectRetryEvent("canceled_echo", _direct_retries[i].trigger_packet, 0); + onDirectRetryEvent("good", _direct_retries[i].trigger_packet, 0); clearDirectRetrySlot(i); } cleared = true; @@ -503,7 +515,35 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { if (_direct_retries[i].queued) { if (_direct_retries[i].packet == packet) { // The retry packet itself just finished transmitting; Dispatcher will release it after this hook. - clearDirectRetrySlot(i); + onDirectRetryEvent("resent", packet, 0); + _direct_retries[i].retry_attempts_sent++; + if (_direct_retries[i].retry_attempts_sent >= DIRECT_RETRY_MAX_ATTEMPTS) { + onDirectRetryEvent("failure", packet, 0); + clearDirectRetrySlot(i); + continue; + } + + Packet* retry = obtainNewPacket(); + if (retry == NULL) { + onDirectRetryEvent("dropped_no_packet", packet, 0); + onDirectRetryEvent("failure", packet, 0); + clearDirectRetrySlot(i); + continue; + } + + *retry = *packet; + uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[_direct_retries[i].retry_attempts_sent]; + sendPacket(retry, _direct_retries[i].priority, retry_delay); + if (isDirectRetryQueued(retry)) { + _direct_retries[i].packet = retry; + _direct_retries[i].retry_delay = retry_delay; + _direct_retries[i].retry_at = futureMillis(retry_delay); + onDirectRetryEvent("queued", retry, retry_delay); + } else { + onDirectRetryEvent("dropped_queue_full", retry, retry_delay); + onDirectRetryEvent("failure", retry, 0); + clearDirectRetrySlot(i); + } } continue; } @@ -515,6 +555,8 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { // Allocate the retry packet only after TX-complete so busy repeaters do not reserve pool slots early. Packet* retry = obtainNewPacket(); if (retry == NULL) { + onDirectRetryEvent("dropped_no_packet", packet, _direct_retries[i].retry_delay); + onDirectRetryEvent("failure", packet, 0); clearDirectRetrySlot(i); continue; } @@ -528,7 +570,10 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { _direct_retries[i].trigger_packet = NULL; _direct_retries[i].queued = true; _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); + onDirectRetryEvent("queued", retry, _direct_retries[i].retry_delay); } else { + onDirectRetryEvent("dropped_queue_full", retry, _direct_retries[i].retry_delay); + onDirectRetryEvent("failure", retry, 0); clearDirectRetrySlot(i); } } @@ -543,12 +588,16 @@ void Mesh::clearPendingDirectRetryOnSendFail(const Packet* packet) { if (_direct_retries[i].queued) { if (_direct_retries[i].packet == packet) { // The queued retry itself failed; Dispatcher will release it after this hook. + onDirectRetryEvent("dropped_send_fail", packet, 0); + onDirectRetryEvent("failure", packet, 0); clearDirectRetrySlot(i); } continue; } if (_direct_retries[i].trigger_packet == packet) { + onDirectRetryEvent("dropped_send_fail", packet, 0); + onDirectRetryEvent("failure", packet, 0); clearDirectRetrySlot(i); } } @@ -631,17 +680,19 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { } // Only store retry metadata here; allocate the retry packet after the initial TX really completes. - uint32_t retry_delay = getDirectRetryEchoDelay(packet); + uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[0]; calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key); _direct_retries[slot_idx].packet = NULL; _direct_retries[slot_idx].trigger_packet = const_cast(packet); _direct_retries[slot_idx].retry_at = 0; _direct_retries[slot_idx].retry_delay = retry_delay; + _direct_retries[slot_idx].retry_attempts_sent = 0; _direct_retries[slot_idx].priority = priority; _direct_retries[slot_idx].progress_marker = progress_marker; _direct_retries[slot_idx].expect_path_growth = expect_path_growth; _direct_retries[slot_idx].queued = false; _direct_retries[slot_idx].active = true; + onDirectRetryEvent("armed", packet, retry_delay); } Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) { @@ -983,4 +1034,3 @@ void Mesh::sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay } } - diff --git a/src/Mesh.h b/src/Mesh.h index 94bfec9f1b..4441514b5e 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -34,6 +34,7 @@ class Mesh : public Dispatcher { Packet* trigger_packet; unsigned long retry_at; uint32_t retry_delay; + uint8_t retry_attempts_sent; uint8_t retry_key[MAX_HASH_SIZE]; uint8_t priority; uint8_t progress_marker; @@ -111,6 +112,11 @@ class Mesh : public Dispatcher { */ virtual uint8_t getExtraAckTransmitCount() const; + /** + * \brief Optional hook for logging direct-retry lifecycle events. + */ + virtual void onDirectRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis) { } + /** * \brief Perform search of local DB of peers/contacts. * \returns Number of peers with matching hash diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 217fd5a08c..f5da272b1b 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -13,6 +13,8 @@ class SimpleMeshTables : public mesh::MeshTables { public: + typedef bool (*RecentRepeaterAllowFn)(const uint8_t* prefix, uint8_t prefix_len, void* ctx); + struct RecentRepeaterInfo { // Just enough identity to match a next-hop path prefix plus the SNR that heard it. uint8_t prefix[MAX_ROUTE_HASH_BYTES]; @@ -28,6 +30,9 @@ class SimpleMeshTables : public mesh::MeshTables { uint32_t _direct_dups, _flood_dups; RecentRepeaterInfo _recent_repeaters[MAX_RECENT_REPEATERS]; int _next_recent_repeater_idx; + int8_t _recent_repeater_min_snr_x4; + RecentRepeaterAllowFn _recent_repeater_allow_fn; + void* _recent_repeater_allow_ctx; bool hasSeenAck(uint32_t ack) const { for (int i = 0; i < MAX_PACKET_ACKS; i++) { @@ -58,6 +63,11 @@ class SimpleMeshTables : public mesh::MeshTables { _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; } + bool prefixesOverlap(const uint8_t* a, uint8_t a_len, const uint8_t* b, uint8_t b_len) const { + uint8_t n = a_len < b_len ? a_len : b_len; + return n > 0 && memcmp(a, b, n) == 0; + } + bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { // Learn repeater prefixes only from packet shapes that expose a trustworthy repeater ID. if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->payload_len >= PUB_KEY_SIZE) { @@ -96,14 +106,10 @@ class SimpleMeshTables : public mesh::MeshTables { if (!extractRecentRepeater(packet, prefix, prefix_len) || prefix_len == 0) { return; } - - // Ring buffer is enough here; retry fallback only needs a recent prefix->SNR observation. - RecentRepeaterInfo& slot = _recent_repeaters[_next_recent_repeater_idx]; - memset(slot.prefix, 0, sizeof(slot.prefix)); - memcpy(slot.prefix, prefix, prefix_len); - slot.prefix_len = prefix_len; - slot.snr_x4 = packet->_snr; - _next_recent_repeater_idx = (_next_recent_repeater_idx + 1) % MAX_RECENT_REPEATERS; + if (packet->_snr < _recent_repeater_min_snr_x4) { + return; + } + setRecentRepeater(prefix, prefix_len, packet->_snr); } public: @@ -115,6 +121,9 @@ class SimpleMeshTables : public mesh::MeshTables { _direct_dups = _flood_dups = 0; memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); _next_recent_repeater_idx = 0; + _recent_repeater_min_snr_x4 = -128; + _recent_repeater_allow_fn = NULL; + _recent_repeater_allow_ctx = NULL; } #ifdef ESP32 @@ -216,19 +225,74 @@ class SimpleMeshTables : public mesh::MeshTables { uint32_t getNumDirectDups() const { return _direct_dups; } uint32_t getNumFloodDups() const { return _flood_dups; } + void setRecentRepeaterMinSNRX4(int8_t min_snr_x4) { + _recent_repeater_min_snr_x4 = min_snr_x4; + } + void setRecentRepeaterAllowFilter(RecentRepeaterAllowFn fn, void* ctx) { + _recent_repeater_allow_fn = fn; + _recent_repeater_allow_ctx = ctx; + } + bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) { + if (prefix == NULL || prefix_len == 0) { + return false; + } + + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + if (_recent_repeater_allow_fn != NULL && !_recent_repeater_allow_fn(prefix, prefix_len, _recent_repeater_allow_ctx)) { + return false; + } + + // Keep one slot for overlapping prefixes so 1/2/3-byte paths share the same entry. + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (prefix_len > existing.prefix_len) { + memset(existing.prefix, 0, sizeof(existing.prefix)); + memcpy(existing.prefix, prefix, prefix_len); + existing.prefix_len = prefix_len; + } + existing.snr_x4 = snr_x4; + return true; + } + + // Ring buffer is enough here; retry fallback only needs a recent prefix->SNR observation. + RecentRepeaterInfo& slot = _recent_repeaters[_next_recent_repeater_idx]; + memset(slot.prefix, 0, sizeof(slot.prefix)); + memcpy(slot.prefix, prefix, prefix_len); + slot.prefix_len = prefix_len; + slot.snr_x4 = snr_x4; + _next_recent_repeater_idx = (_next_recent_repeater_idx + 1) % MAX_RECENT_REPEATERS; + return true; + } + const RecentRepeaterInfo* getLatestRecentRepeater() const { + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; + const RecentRepeaterInfo* info = &_recent_repeaters[idx]; + if (info->prefix_len > 0) { + return info; + } + } + return NULL; + } + const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t* hash, uint8_t hash_len) const { if (hash == NULL || hash_len == 0) { return NULL; } - // Search newest-to-oldest so the retry gate prefers the freshest SNR sample for a prefix. + // Search newest-to-oldest and allow 1/2/3-byte prefixes to overlap-match. for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; const RecentRepeaterInfo* info = &_recent_repeaters[idx]; - if (info->prefix_len < hash_len || info->prefix_len == 0) { + if (info->prefix_len == 0) { continue; } - if (memcmp(info->prefix, hash, hash_len) == 0) { + if (prefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) { return info; } } From 9c2ac5aa1c7b8448f9189813120879d2b00a63fc Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 24 Apr 2026 00:08:10 -0700 Subject: [PATCH 08/94] Max retries is now a var that can be set between 1 to 15 --- docs/cli_commands.md | 39 +++++++- examples/simple_repeater/MyMesh.cpp | 134 ++++++++++++++++++++++++---- examples/simple_repeater/MyMesh.h | 1 + src/Mesh.cpp | 30 +++++-- src/Mesh.h | 10 +++ src/helpers/CommonCLI.cpp | 49 +++++++++- src/helpers/CommonCLI.h | 3 + src/helpers/SimpleMeshTables.h | 45 ++++++++++ 8 files changed, 283 insertions(+), 28 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 774066b0a1..e798cf0e86 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -117,14 +117,21 @@ This document provides an overview of CLI commands that can be sent to MeshCore ### Get or set recent repeater fallback prefix/SNR **Usage:** -- `recent.repeater` -- `recent.repeater ` +- `get recent.repeater` +- `get recent.repeater all` +- `get recent.repeater first ` +- `get recent.repeater last ` +- `set recent.repeater ` **Parameters:** - `prefix_hex`: 1-3 bytes of next-hop prefix (hex) - `snr_db`: SNR in dB (supports decimals; stored at x4 precision) +- `count`: number of entries to print -**Note:** `set` is rejected when the prefix already exists in neighbors. +**Notes:** +- `set` is rejected when the prefix already exists in neighbors. +- `all` prints oldest to newest; `first` prints the oldest N; `last` prints the newest N. +- Remote CLI replies include rows too, but may truncate when the packet payload limit is reached. --- @@ -545,6 +552,32 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change the number of direct retry attempts +**Usage:** +- `get direct.retry.count` +- `set direct.retry.count ` + +**Parameters:** +- `value`: Retry attempts after initial TX (`1`-`15`) + +**Default:** `3` + +--- + +#### View or change the base direct retry wait (milliseconds) +**Usage:** +- `get direct.retry.base` +- `set direct.retry.base ` + +**Parameters:** +- `value`: Base wait in milliseconds (`10`-`5000`) + +**Default:** `200` + +**Note:** The actual first retry wait is `base + computed_echo_wait_from_live_phy`. + +--- + #### [Experimental] View or change the processing delay for received traffic **Usage:** - `get rxdelay` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 71b532ed86..220f970421 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -627,16 +627,21 @@ bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_ho return recent != NULL && recent->snr_x4 >= min_snr_x4; } uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { + uint32_t base_wait_millis = constrain((uint32_t)_prefs.direct_retry_base_ms, (uint32_t)10, (uint32_t)5000); // Approximate LoRa line rate in kilobits/sec from the live radio params the repeater is using now. float kbps = (((float) active_sf) * active_bw * ((float) active_cr)) / ((float) (1UL << active_sf)); if (kbps <= 0.0f) { - return HALO_DIRECT_RETRY_DELAY_MIN; + return base_wait_millis; } // Wait roughly long enough for our transmission, the next hop's receive/forward window, and its echo back. uint32_t bits = ((uint32_t) packet->getRawLength()) * 8; uint32_t scaled_wait_millis = (uint32_t) ((((float) bits) * 4.0f) / kbps); - return max((uint32_t) HALO_DIRECT_RETRY_DELAY_MIN, scaled_wait_millis); + return base_wait_millis + scaled_wait_millis; +} +uint8_t MyMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const { + (void)packet; + return constrain(_prefs.direct_retry_attempts, (uint8_t)1, (uint8_t)15); } bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { @@ -970,6 +975,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.direct_tx_delay_factor = 0.3f; // was 0.2 _prefs.direct_retry_recent_enabled = 0; _prefs.direct_retry_snr_margin_db = 5; + _prefs.direct_retry_attempts = 3; + _prefs.direct_retry_base_ms = 200; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; _prefs.node_lon = ADVERT_LON; @@ -1345,29 +1352,36 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply Serial.printf("\n"); } reply[0] = 0; - } else if (memcmp(command, "recent.repeater", 15) == 0) { - const char* sub = command + 15; + } else if (strncmp(command, "get recent.repeater", 19) == 0 + || strncmp(command, "set recent.repeater", 19) == 0 + || strncmp(command, "recent.repeater", 15) == 0) { + bool is_get = false; + bool is_set = false; + const char* sub = command; + + if (strncmp(command, "get recent.repeater", 19) == 0) { + is_get = true; + sub = command + 19; + } else if (strncmp(command, "set recent.repeater", 19) == 0) { + is_set = true; + sub = command + 19; + } else { + sub = command + 15; // legacy command format + } while (*sub == ' ') sub++; + auto* tables = (SimpleMeshTables*)getTables(); - if (*sub == 0) { - const auto* info = tables->getLatestRecentRepeater(); - if (info == NULL) { - strcpy(reply, "> none"); - } else { - char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; - mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); - } - } else { + + if (is_set || (!is_get && *sub != 0 && strcmp(sub, "all") != 0 && strncmp(sub, "first ", 6) != 0 && strncmp(sub, "last ", 5) != 0)) { char* params = (char*) sub; char* arg_snr = strchr(params, ' '); if (arg_snr == NULL) { - strcpy(reply, "Err - usage: recent.repeater "); + strcpy(reply, "Err - usage: set recent.repeater "); } else { *arg_snr++ = 0; while (*arg_snr == ' ') arg_snr++; if (*arg_snr == 0) { - strcpy(reply, "Err - usage: recent.repeater "); + strcpy(reply, "Err - usage: set recent.repeater "); } else { int hex_len = strlen(params); int prefix_len = hex_len / 2; @@ -1386,6 +1400,94 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } } + } else if (*sub == 0) { + const auto* info = tables->getLatestRecentRepeater(); + if (info == NULL) { + strcpy(reply, "> none"); + } else { + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; + mesh::Utils::toHex(hex, info->prefix, info->prefix_len); + sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + } + } else if (strcmp(sub, "all") == 0 || strncmp(sub, "first ", 6) == 0 || strncmp(sub, "last ", 5) == 0) { + int total = tables->getRecentRepeaterCount(); + if (total <= 0) { + strcpy(reply, "> none"); + } else { + bool newest_first = false; + int limit = total; + const char* mode = "all"; + if (strncmp(sub, "first ", 6) == 0 || strncmp(sub, "last ", 5) == 0) { + const char* nstr = sub + (sub[0] == 'f' ? 6 : 5); + while (*nstr == ' ') nstr++; + if (*nstr == 0) { + strcpy(reply, "Err - usage: get recent.repeater first|last "); + return; + } + char* end_ptr = NULL; + long parsed = strtol(nstr, &end_ptr, 10); + while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; + if (end_ptr == NULL || *end_ptr != 0 || parsed <= 0) { + strcpy(reply, "Err - count must be > 0"); + return; + } + limit = (int)parsed; + if (sub[0] == 'l') { + newest_first = true; + mode = "last"; + } else { + mode = "first"; + } + } + if (limit > total) { + limit = total; + } + + if (sender_timestamp == 0) { + Serial.printf("Recent repeater table (%s %d/%d):\n", mode, limit, total); + for (int i = 0; i < limit; i++) { + const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(i) : tables->getRecentRepeaterOldestByIdx(i); + if (info == NULL) { + continue; + } + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; + mesh::Utils::toHex(hex, info->prefix, info->prefix_len); + Serial.printf("%02d: %s,%s\n", i + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + } + sprintf(reply, "> showing %d/%d (%s)", limit, total, mode); + } else { + // Remote CLI replies are packet-bound, so include as many rows as fit. + int written = snprintf(reply, 160, "> showing %d/%d (%s)", limit, total, mode); + bool truncated = false; + if (written < 0) { + reply[0] = 0; + written = 0; + } + for (int i = 0; i < limit; i++) { + const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(i) : tables->getRecentRepeaterOldestByIdx(i); + if (info == NULL) { + continue; + } + if (written >= 154) { + truncated = true; + break; + } + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; + mesh::Utils::toHex(hex, info->prefix, info->prefix_len); + int n = snprintf(reply + written, 160 - written, "\n%02d:%s,%s", i + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + if (n < 0 || n >= (160 - written)) { + truncated = true; + break; + } + written += n; + } + if (truncated && written < 156) { + snprintf(reply + written, 160 - written, "\n..."); + } + } + } + } else { + strcpy(reply, "Err - usage: get recent.repeater [all|first |last ]"); } } else if (memcmp(command, "discover.neighbors", 18) == 0) { const char* sub = command + 18; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 16566dca25..00a8a31b69 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -154,6 +154,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override; uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override; + uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override; void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis) override; int getInterferenceThreshold() const override { diff --git a/src/Mesh.cpp b/src/Mesh.cpp index b9892eedd3..47fc6e8dfe 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -3,8 +3,8 @@ namespace mesh { -static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS = 3; -static const uint32_t DIRECT_RETRY_BACKOFF_MS[DIRECT_RETRY_MAX_ATTEMPTS] = { 200, 300, 400 }; +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 3; +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; void Mesh::begin() { for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { @@ -57,6 +57,14 @@ uint32_t Mesh::getDirectRetryEchoDelay(const Packet* packet) const { // Keep the base fallback aligned with the repeater's minimum retry wait. return 200; } +uint8_t Mesh::getDirectRetryMaxAttempts(const Packet* packet) const { + return DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT; +} +uint32_t Mesh::getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) const { + uint32_t base = getDirectRetryEchoDelay(packet); + // Keep the historical linear spacing while allowing the base wait to vary by platform/profile. + return base + ((uint32_t)attempt_idx * 100UL); +} uint8_t Mesh::getExtraAckTransmitCount() const { return 0; } @@ -517,7 +525,13 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { // The retry packet itself just finished transmitting; Dispatcher will release it after this hook. onDirectRetryEvent("resent", packet, 0); _direct_retries[i].retry_attempts_sent++; - if (_direct_retries[i].retry_attempts_sent >= DIRECT_RETRY_MAX_ATTEMPTS) { + uint8_t max_attempts = getDirectRetryMaxAttempts(packet); + if (max_attempts < 1) { + max_attempts = 1; + } else if (max_attempts > DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX) { + max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX; + } + if (_direct_retries[i].retry_attempts_sent >= max_attempts) { onDirectRetryEvent("failure", packet, 0); clearDirectRetrySlot(i); continue; @@ -532,7 +546,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { } *retry = *packet; - uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[_direct_retries[i].retry_attempts_sent]; + uint32_t retry_delay = getDirectRetryAttemptDelay(packet, _direct_retries[i].retry_attempts_sent); sendPacket(retry, _direct_retries[i].priority, retry_delay); if (isDirectRetryQueued(retry)) { _direct_retries[i].packet = retry; @@ -612,7 +626,9 @@ bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_h case PAYLOAD_TYPE_RESPONSE: case PAYLOAD_TYPE_TXT_MSG: case PAYLOAD_TYPE_ANON_REQ: - if (packet->getPathHashCount() <= 1) { + // Allow retries even when only one downstream hop remains so fixed direct paths + // (e.g. remote admin/login over 2-hop chains) use the same retry policy. + if (packet->getPathHashCount() == 0) { return false; } next_hop_hash = packet->path; @@ -622,7 +638,7 @@ bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_h return true; case PAYLOAD_TYPE_MULTIPART: - if (packet->payload_len < 1 || (packet->payload[0] & 0x0F) != PAYLOAD_TYPE_ACK || packet->getPathHashCount() <= 1) { + if (packet->payload_len < 1 || (packet->payload[0] & 0x0F) != PAYLOAD_TYPE_ACK || packet->getPathHashCount() == 0) { return false; } next_hop_hash = packet->path; @@ -680,7 +696,7 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { } // Only store retry metadata here; allocate the retry packet after the initial TX really completes. - uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[0]; + uint32_t retry_delay = getDirectRetryAttemptDelay(packet, 0); calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key); _direct_retries[slot_idx].packet = NULL; _direct_retries[slot_idx].trigger_packet = const_cast(packet); diff --git a/src/Mesh.h b/src/Mesh.h index 4441514b5e..ad4d8a2f53 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -107,6 +107,16 @@ class Mesh : public Dispatcher { */ virtual uint32_t getDirectRetryEchoDelay(const Packet* packet) const; + /** + * \returns maximum number of retry transmissions after the initial direct TX. + */ + virtual uint8_t getDirectRetryMaxAttempts(const Packet* packet) const; + + /** + * \returns delay before a specific retry attempt, where attempt_idx=0 is the first retry. + */ + virtual uint32_t getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) const; + /** * \returns number of extra (Direct) ACK transmissions wanted. */ diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 38d4536a84..02d27830d8 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -14,6 +14,14 @@ #define DIRECT_RETRY_RECENT_DEFAULT 0 #define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT 5 #define DIRECT_RETRY_SNR_MARGIN_DB_MAX 40 +#define DIRECT_RETRY_TIMING_MAGIC_0 0xD5 +#define DIRECT_RETRY_TIMING_MAGIC_1 0x54 +#define DIRECT_RETRY_COUNT_DEFAULT 3 +#define DIRECT_RETRY_COUNT_MIN 1 +#define DIRECT_RETRY_COUNT_MAX 15 +#define DIRECT_RETRY_BASE_MS_DEFAULT 200 +#define DIRECT_RETRY_BASE_MS_MIN 10 +#define DIRECT_RETRY_BASE_MS_MAX 5000 // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { @@ -97,7 +105,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + file.read((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 291 + file.read((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 292 + file.read((uint8_t *)&_prefs->direct_retry_timing_magic[0], sizeof(_prefs->direct_retry_timing_magic)); // 294 + // next: 296 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -121,6 +132,14 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1); _prefs->direct_retry_snr_margin_db = constrain(_prefs->direct_retry_snr_margin_db, 0, DIRECT_RETRY_SNR_MARGIN_DB_MAX); } + if (_prefs->direct_retry_timing_magic[0] != DIRECT_RETRY_TIMING_MAGIC_0 + || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1) { + _prefs->direct_retry_attempts = DIRECT_RETRY_COUNT_DEFAULT; + _prefs->direct_retry_base_ms = DIRECT_RETRY_BASE_MS_DEFAULT; + } else { + _prefs->direct_retry_attempts = constrain(_prefs->direct_retry_attempts, DIRECT_RETRY_COUNT_MIN, DIRECT_RETRY_COUNT_MAX); + _prefs->direct_retry_base_ms = constrain(_prefs->direct_retry_base_ms, DIRECT_RETRY_BASE_MS_MIN, DIRECT_RETRY_BASE_MS_MAX); + } // sanitise bad bridge pref values _prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1); @@ -201,7 +220,11 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + file.write((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 291 + file.write((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 292 + uint8_t retry_timing_magic[2] = { DIRECT_RETRY_TIMING_MAGIC_0, DIRECT_RETRY_TIMING_MAGIC_1 }; + file.write(retry_timing_magic, sizeof(retry_timing_magic)); // 294 + // next: 296 file.close(); } @@ -363,6 +386,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); } else if (memcmp(config, "direct.retry.margin", 19) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_snr_margin_db); + } else if (memcmp(config, "direct.retry.count", 18) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts); + } else if (memcmp(config, "direct.retry.base", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_base_ms); } else if (memcmp(config, "owner.info", 10) == 0) { *reply++ = '>'; *reply++ = ' '; @@ -633,6 +660,24 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { sprintf(reply, "Error, min 0 and max %d", DIRECT_RETRY_SNR_MARGIN_DB_MAX); } + } else if (memcmp(config, "direct.retry.count ", 19) == 0) { + int count = atoi(&config[19]); + if (count >= DIRECT_RETRY_COUNT_MIN && count <= DIRECT_RETRY_COUNT_MAX) { + _prefs->direct_retry_attempts = (uint8_t)count; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_COUNT_MIN, DIRECT_RETRY_COUNT_MAX); + } + } else if (memcmp(config, "direct.retry.base ", 18) == 0) { + int delay_ms = atoi(&config[18]); + if (delay_ms >= DIRECT_RETRY_BASE_MS_MIN && delay_ms <= DIRECT_RETRY_BASE_MS_MAX) { + _prefs->direct_retry_base_ms = (uint16_t)delay_ms; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_BASE_MS_MIN, DIRECT_RETRY_BASE_MS_MAX); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 85962638fd..03b1fb649b 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -63,6 +63,9 @@ struct NodePrefs { // persisted to file uint8_t rx_boosted_gain; // power settings uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; + uint8_t direct_retry_attempts; + uint16_t direct_retry_base_ms; + uint8_t direct_retry_timing_magic[2]; }; class CommonCLICallbacks { diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index f5da272b1b..705869ad35 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -279,6 +279,51 @@ class SimpleMeshTables : public mesh::MeshTables { } return NULL; } + int getRecentRepeaterCount() const { + int count = 0; + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + if (_recent_repeaters[i].prefix_len > 0) { + count++; + } + } + return count; + } + const RecentRepeaterInfo* getRecentRepeaterNewestByIdx(int idx_wanted) const { + if (idx_wanted < 0) { + return NULL; + } + int idx_seen = 0; + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; + const RecentRepeaterInfo* info = &_recent_repeaters[idx]; + if (info->prefix_len == 0) { + continue; + } + if (idx_seen == idx_wanted) { + return info; + } + idx_seen++; + } + return NULL; + } + const RecentRepeaterInfo* getRecentRepeaterOldestByIdx(int idx_wanted) const { + if (idx_wanted < 0) { + return NULL; + } + int idx_seen = 0; + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx + i) % MAX_RECENT_REPEATERS; + const RecentRepeaterInfo* info = &_recent_repeaters[idx]; + if (info->prefix_len == 0) { + continue; + } + if (idx_seen == idx_wanted) { + return info; + } + idx_seen++; + } + return NULL; + } const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t* hash, uint8_t hash_len) const { if (hash == NULL || hash_len == 0) { From c5d8ada27d4e9a2146442fe5ce4326047d011832 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 24 Apr 2026 23:02:11 +0700 Subject: [PATCH 09/94] Added features from PowerSaving 14.1.1 --- build-iotthinks.sh | 96 +++ build.sh | 7 +- examples/companion_radio/MyMesh.cpp | 39 +- examples/companion_radio/MyMesh.h | 5 +- examples/companion_radio/NodePrefs.h | 1 + examples/companion_radio/main.cpp | 44 +- examples/simple_repeater/MyMesh.cpp | 2 + examples/simple_repeater/main.cpp | 20 +- examples/simple_room_server/MyMesh.cpp | 10 + examples/simple_room_server/MyMesh.h | 3 + examples/simple_room_server/main.cpp | 13 + src/MeshCore.h | 4 + src/helpers/CommonCLI.cpp | 557 ++++++++++-------- src/helpers/CommonCLI.h | 1 + src/helpers/ESP32Board.h | 142 ++++- src/helpers/SensorManager.h | 2 + src/helpers/esp32/TBeamBoard.h | 14 +- .../sensors/EnvironmentSensorManager.cpp | 133 +++-- .../sensors/EnvironmentSensorManager.h | 1 + variants/gat562_30s_mesh_kit/platformio.ini | 2 +- .../gat562_mesh_tracker_pro/platformio.ini | 2 +- variants/heltec_t114/platformio.ini | 2 +- .../HeltecTrackerV2Board.cpp | 18 + .../heltec_tracker_v2/HeltecTrackerV2Board.h | 3 + variants/heltec_tracker_v2/LoRaFEMControl.h | 3 +- variants/heltec_v2/HeltecV2Board.h | 16 +- variants/heltec_v2/platformio.ini | 31 +- variants/heltec_v2/target.cpp | 4 +- variants/heltec_v3/platformio.ini | 26 + variants/heltec_v4/HeltecV4Board.cpp | 24 +- variants/heltec_v4/HeltecV4Board.h | 3 + variants/heltec_v4/LoRaFEMControl.h | 3 +- variants/heltec_v4/platformio.ini | 76 ++- .../LilygoT3S3SX1276Board.h | 10 + variants/lilygo_t3s3_sx1276/target.cpp | 2 +- variants/lilygo_t3s3_sx1276/target.h | 4 +- variants/lilygo_tbeam_SX1262/platformio.ini | 7 + .../LilygoTBeamSX1276Board.h | 10 + variants/lilygo_tbeam_SX1276/platformio.ini | 7 + variants/lilygo_tbeam_SX1276/target.cpp | 2 +- variants/lilygo_tbeam_SX1276/target.h | 4 +- .../platformio.ini | 7 + variants/lilygo_tlora_v2_1/LilyGoTLoraBoard.h | 4 + variants/rak3401/platformio.ini | 2 +- variants/rak4631/platformio.ini | 2 +- variants/sensecap_solar/platformio.ini | 2 +- variants/xiao_c3/platformio.ini | 22 + variants/xiao_nrf52/platformio.ini | 2 +- variants/xiao_s3/XiaoS3Board.h | 13 + variants/xiao_s3/platformio.ini | 180 ++++++ variants/xiao_s3/target.cpp | 56 ++ variants/xiao_s3/target.h | 30 + variants/xiao_s3_wio/platformio.ini | 26 + 53 files changed, 1319 insertions(+), 380 deletions(-) create mode 100644 build-iotthinks.sh create mode 100644 variants/lilygo_t3s3_sx1276/LilygoT3S3SX1276Board.h create mode 100644 variants/lilygo_tbeam_SX1276/LilygoTBeamSX1276Board.h create mode 100644 variants/xiao_s3/XiaoS3Board.h create mode 100644 variants/xiao_s3/platformio.ini create mode 100644 variants/xiao_s3/target.cpp create mode 100644 variants/xiao_s3/target.h diff --git a/build-iotthinks.sh b/build-iotthinks.sh new file mode 100644 index 0000000000..7c654482c7 --- /dev/null +++ b/build-iotthinks.sh @@ -0,0 +1,96 @@ +# sh ./build-repeaters-iotthinks.sh +export FIRMWARE_VERSION="PowerSaving15" + +############# Repeaters ############# +# Commonly-used boards +## ESP32 - 12 boards +sh build.sh build-firmware \ +Heltec_v3_repeater \ +Heltec_WSL3_repeater \ +heltec_v4_repeater \ +Station_G2_repeater \ +T_Beam_S3_Supreme_SX1262_repeater \ +Tbeam_SX1262_repeater \ +LilyGo_T3S3_sx1262_repeater \ +Xiao_S3_WIO_repeater \ +Xiao_C3_repeater \ +Xiao_C6_repeater_ \ +Heltec_E290_repeater \ +Heltec_Wireless_Tracker_repeater + +## NRF52 - 13 boards +sh build.sh build-firmware \ +RAK_4631_repeater \ +Heltec_t114_repeater \ +Xiao_nrf52_repeater \ +Heltec_mesh_solar_repeater \ +ProMicro_repeater \ +SenseCap_Solar_repeater \ +t1000e_repeater \ +LilyGo_T-Echo_repeater \ +WioTrackerL1_repeater \ +RAK_3401_repeater \ +RAK_WisMesh_Tag_repeater \ +GAT562_30S_Mesh_Kit_repeater \ +GAT562_Mesh_Tracker_Pro_repeater + +## ESP32, SX1276 - 3 boards +sh build.sh build-firmware \ +Heltec_v2_repeater \ +LilyGo_TLora_V2_1_1_6_repeater \ +Tbeam_SX1276_repeater + +## Ikoka - 3 boards +sh build.sh build-firmware \ +ikoka_nano_nrf_22dbm_repeater \ +ikoka_nano_nrf_30dbm_repeater \ +ikoka_nano_nrf_33dbm_repeater + +############# Room Server ############# +# ESP32 +sh build.sh build-firmware \ +Heltec_v3_room_server \ +heltec_v4_room_server + +# NRF52 +sh build.sh build-firmware \ +RAK_4631_room_server \ +Heltec_t114_room_server \ +Xiao_nrf52_room_server \ +t1000e_room_server \ +WioTrackerL1_room_server \ +RAK_3401_room_server + +############# Companions BLE ############# +# ESP32 +sh build.sh build-firmware \ +Heltec_v3_companion_radio_ble_ps \ +heltec_v4_companion_radio_ble_ps \ +heltec_v4_companion_radio_ble_ps_femoff \ +Xiao_S3_WIO_companion_radio_ble \ +Heltec_Wireless_Paper_companion_radio_ble + +# NRF52 +sh build.sh build-firmware \ +RAK_4631_companion_radio_ble \ +Heltec_t114_companion_radio_ble \ +Xiao_nrf52_companion_radio_ble \ +t1000e_companion_radio_ble \ +LilyGo_T-Echo_companion_radio_ble \ +WioTrackerL1_companion_radio_ble \ +RAK_3401_companion_radio_ble \ +RAK_WisMesh_Tag_companion_radio_ble + +############# Companions USB ############# +sh build.sh build-firmware \ +Heltec_v3_companion_radio_usb + +############# Companions BLE PS ############# +sh build.sh build-firmware \ +Heltec_v3_companion_radio_ble_ps \ +heltec_v4_companion_radio_ble_ps \ +heltec_v4_3_companion_radio_ble_ps_femoff \ +Xiao_C3_companion_radio_ble_ps \ +Xiao_S3_companion_radio_ble_ps \ +Xiao_S3_WIO_companion_radio_ble_ps \ +Heltec_v2_companion_radio_ble_ps diff --git a/build.sh b/build.sh index 313c4c47a0..006eae9698 100755 --- a/build.sh +++ b/build.sh @@ -134,7 +134,8 @@ build_firmware() { # set firmware version string # e.g: v1.0.0-abcdef - FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}-${COMMIT_HASH}" + # FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}-${COMMIT_HASH}" + FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}" # craft filename # e.g: RAK_4631_Repeater-v1.0.0-SHA @@ -152,8 +153,8 @@ build_firmware() { # build merge-bin for esp32 fresh install, copy .bins to out folder (e.g: Heltec_v3_room_server-v1.0.0-SHA.bin) if [ "$ENV_PLATFORM" == "ESP32_PLATFORM" ]; then pio run -t mergebin -e $1 - cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true - cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-merged.bin 2>/dev/null || true + cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}-upgrade.bin 2>/dev/null || true + cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-freshInstall-merged.bin 2>/dev/null || true fi # build .uf2 for nrf52 boards, copy .uf2 and .zip to out folder (e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index e8c1914bad..67ff0a81ce 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -46,7 +46,9 @@ #define CMD_SET_CUSTOM_VAR 41 #define CMD_GET_ADVERT_PATH 42 #define CMD_GET_TUNING_PARAMS 43 -// NOTE: CMD range 44..49 parked, potentially for WiFi operations +#define CMD_GET_RADIO_FEM_RXGAIN 44 +#define CMD_SET_RADIO_FEM_RXGAIN 45 +// NOTE: CMD range 46..49 parked, potentially for WiFi operations #define CMD_SEND_BINARY_REQ 50 #define CMD_FACTORY_RESET 51 #define CMD_SEND_PATH_DISCOVERY_REQ 52 @@ -876,6 +878,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.rx_boosted_gain = 1; // enabled by default #endif #endif + _prefs.radio_fem_rxgain = 1; } void MyMesh::begin(bool has_display) { @@ -925,6 +928,7 @@ void MyMesh::begin(bool has_display) { _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER); _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1 _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours + _prefs.radio_fem_rxgain = constrain(_prefs.radio_fem_rxgain, 0, 1); #ifdef BLE_PIN_CODE // 123456 by default if (_prefs.ble_pin == 0) { @@ -954,6 +958,7 @@ void MyMesh::begin(bool has_display) { radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_set_tx_power(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); + board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); } @@ -1798,6 +1803,30 @@ void MyMesh::handleCmdFrame(size_t len) { } else { writeErrFrame(ERR_CODE_ILLEGAL_ARG); } + } else if (cmd_frame[0] == CMD_GET_RADIO_FEM_RXGAIN) { + if (!board.canControlLoRaFemLna()) { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + } else { + out_frame[0] = RESP_CODE_OK; + uint32_t value = board.isLoRaFemLnaEnabled() ? 1 : 0; + memcpy(&out_frame[1], &value, 4); + _serial->writeFrame(out_frame, 5); + } + } else if (cmd_frame[0] == CMD_SET_RADIO_FEM_RXGAIN && len >= 2) { + uint8_t value = cmd_frame[1]; + if (!board.canControlLoRaFemLna()) { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + } else if (value <= 1) { + _prefs.radio_fem_rxgain = value; + if (board.setLoRaFemLnaEnabled(value != 0)) { + savePrefs(); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + } + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } } else if (cmd_frame[0] == CMD_GET_ADVERT_PATH && len >= PUB_KEY_SIZE+2) { // FUTURE use: uint8_t reserved = cmd_frame[1]; uint8_t *pub_key = &cmd_frame[2]; @@ -2191,3 +2220,11 @@ bool MyMesh::advert() { return false; } } + +// To check if there is pending work +bool MyMesh::hasPendingWork() const { +#if defined(WITH_BRIDGE) + if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep +#endif + return _mgr->getOutboundTotal() > 0; +} diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index aeff591cf4..ad0909bcca 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -165,7 +165,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { public: void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); } - + #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0"); @@ -177,6 +177,9 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { } #endif + // To check if there is pending work + bool hasPendingWork() const; + private: void writeOKFrame(); void writeErrFrame(uint8_t err_code); diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 48c381ceaf..6598a69c60 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -29,6 +29,7 @@ struct NodePrefs { // persisted to file uint32_t gps_interval; // GPS read interval in seconds 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 uint8_t client_repeat; uint8_t path_hash_mode; // which path mode to use when sending uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 876dc9c33c..90903097a5 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -2,6 +2,11 @@ #include #include "MyMesh.h" +#ifdef ESP32_PLATFORM +#include "esp_pm.h" +#include "esp_bt.h" +#endif + // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -42,8 +47,8 @@ static uint32_t _atoi(const char* sp) { #define TCP_PORT 5000 #endif #elif defined(BLE_PIN_CODE) - #include - SerialBLEInterface serial_interface; + #include + SerialBLEInterface serial_interface; #elif defined(SERIAL_RX) #include ArduinoSerialInterface serial_interface; @@ -220,6 +225,33 @@ void setup() { #ifdef DISPLAY_CLASS ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved #endif + +#ifdef ESP32_PLATFORM + // Enable BLE sleep + esp_err_t errBLESleep = esp_bt_sleep_enable(); + if (errBLESleep == ESP_OK) { + Serial.println("Bluetooth sleep enabled successfully"); + } else { + Serial.printf("Bluetooth sleep enable failed: %s\n", esp_err_to_name(errBLESleep)); + } + +#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; +#endif + + // Configure Power Management + pm_config = { .max_freq_mhz = 80, .min_freq_mhz = 40, .light_sleep_enable = true }; + esp_err_t errPM = esp_pm_configure(&pm_config); + if (errPM == ESP_OK) { + Serial.println("Power Management configured successfully"); + } else { + Serial.printf("Power Management failed to configure: %d\r\n", errPM); + } +#endif } void loop() { @@ -229,4 +261,12 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + + if (!the_mesh.hasPendingWork()) { +#if defined(NRF52_PLATFORM) + board.sleep(0); // nrf ignores seconds param, sleeps whenever possible +#else if defined(ESP32_PLATFORM) + vTaskDelay(pdMS_TO_TICKS(50)); // attempt to sleep +#endif + } } diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 666f79fc5c..1e9cb91fc6 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -911,6 +911,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.rx_boosted_gain = 1; // enabled by default; #endif #endif + _prefs.radio_fem_rxgain = 1; pending_discover_tag = 0; pending_discover_until = 0; @@ -959,6 +960,7 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); + board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index e37078ce5f..b3fbbe5b65 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -20,8 +20,7 @@ void halt() { static char command[160]; // For power saving -unsigned long lastActive = 0; // mark last active time -unsigned long nextSleepinSecs = 120; // next sleep in seconds. The first sleep (if enabled) is after 2 minutes from boot +unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot #if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_) static unsigned long userBtnDownAt = 0; @@ -40,9 +39,6 @@ void setup() { delay(5000); #endif - // For power saving - lastActive = millis(); // mark last active time since boot - #ifdef DISPLAY_CLASS if (display.begin()) { display.startFrame(); @@ -155,16 +151,12 @@ void loop() { rtc_clock.tick(); if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { - #if defined(NRF52_PLATFORM) +#if defined(NRF52_PLATFORM) board.sleep(1800); // nrf ignores seconds param, sleeps whenever possible - #else - if (the_mesh.millisHasNowPassed(lastActive + nextSleepinSecs * 1000)) { // To check if it is time to sleep - board.sleep(1800); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet - lastActive = millis(); - nextSleepinSecs = 5; // Default: To work for 5s and sleep again - } else { - nextSleepinSecs += 5; // When there is pending work, to work another 5s +#else + if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep + board.sleep(1800); // Sleep. Wake up after 30 minutes or when receiving a LoRa packet } - #endif +#endif } } diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 145fb0fd9f..bdda819453 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -652,6 +652,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.gps_enabled = 0; _prefs.gps_interval = 0; _prefs.advert_loc_policy = ADVERT_LOC_PREFS; + _prefs.radio_fem_rxgain = 1; next_post_idx = 0; next_client_idx = 0; @@ -693,6 +694,7 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_set_tx_power(_prefs.tx_power_dbm); + board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); updateAdvertTimer(); updateFloodAdvertTimer(); @@ -1022,3 +1024,11 @@ void MyMesh::loop() { uptime_millis += now - last_millis; last_millis = now; } + +// To check if there is pending work +bool MyMesh::hasPendingWork() const { +#if defined(WITH_BRIDGE) + if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep +#endif + return _mgr->getOutboundTotal() > 0; +} diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 1b35ae95a1..410ffef019 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -222,4 +222,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void clearStats() override; void handleCommand(uint32_t sender_timestamp, char* command, char* reply); void loop(); + + // To check if there is pending work + bool hasPendingWork() const; }; diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 825fb007d5..5517980936 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -18,6 +18,9 @@ void halt() { static char command[MAX_POST_TEXT_LEN+1]; +// For power saving +unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot + void setup() { Serial.begin(115200); delay(1000); @@ -113,4 +116,14 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + + if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { +#if defined(NRF52_PLATFORM) + board.sleep(1800); // nrf ignores seconds param, sleeps whenever possible +#else + if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep + board.sleep(1800); // Sleep. Wake up after 30 minutes or when receiving a LoRa packet + } +#endif + } } diff --git a/src/MeshCore.h b/src/MeshCore.h index 2db1d4c3ec..a79bd8b024 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -52,12 +52,16 @@ class MainBoard { virtual void onAfterTransmit() { } virtual void reboot() = 0; virtual void powerOff() { /* 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 uint32_t getGpio() { return 0; } virtual void setGpio(uint32_t values) {} virtual uint8_t getStartupReason() const = 0; virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; } virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported + virtual bool setLoRaFemLnaEnabled(bool enable) { return false; } + virtual bool canControlLoRaFemLna() const { return false; } + virtual bool isLoRaFemLnaEnabled() const { return false; } // Power management interface (boards with power management override these) virtual bool isExternalPowered() { return false; } diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index d495aada5f..1aa4265c23 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -3,6 +3,8 @@ #include "TxtDataHelpers.h" #include "AdvertDataHelpers.h" #include +#define STR_HELPER(x) #x +#define STR(x) STR_HELPER(x) #ifndef BRIDGE_MAX_BAUD #define BRIDGE_MAX_BAUD 115200 @@ -118,6 +120,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // sanitise settings _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean + _prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean file.close(); } @@ -180,6 +183,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 // next: 291 + file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 291 + // next: 292 file.close(); } @@ -426,19 +431,59 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } #endif } else if (memcmp(command, "powersaving on", 14) == 0) { +#if defined(NRF52_PLATFORM) _prefs->powersaving_enabled = 1; savePrefs(); - strcpy(reply, "ok"); // TODO: to return Not supported if required + strcpy(reply, "On - Immediate effect"); +#elif defined(ESP32) && !defined(WITH_BRIDGE) + _prefs->powersaving_enabled = 1; + savePrefs(); + strcpy(reply, "On - After 2 minutes"); +#elif defined(WITH_BRIDGE) + strcpy(reply, "Bridge not supported"); +#else + strcpy(reply, "Board not supported"); +#endif } else if (memcmp(command, "powersaving off", 15) == 0) { _prefs->powersaving_enabled = 0; savePrefs(); - strcpy(reply, "ok"); + strcpy(reply, "Off"); } else if (memcmp(command, "powersaving", 11) == 0) { if (_prefs->powersaving_enabled) { - strcpy(reply, "on"); + strcpy(reply, "On"); } else { - strcpy(reply, "off"); + strcpy(reply, "Off"); } + } else if (memcmp(command, "sensor", 6) == 0) { + // I2C +#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 + + // GPS +#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)); +#ifdef 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 (memcmp(command, "powerlog", 8) == 0) { + sprintf(reply, "Last reset reason: %s", _board->getResetReasonString(_board->getResetReason())); +#if defined(NRF52_PLATFORM) + sprintf(reply + strlen(reply), "\r\nLast shutdown reason: %s", + _board->getShutdownReasonString(_board->getShutdownReason())); + sprintf(reply + strlen(reply), "\r\nLast boot voltage: %u mV", _board->getBootVoltage()); +#endif } else if (memcmp(command, "log start", 9) == 0) { _callbacks->setLoggingOn(true); strcpy(reply, " logging on"); @@ -477,257 +522,279 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "OK - %d.%d%%", a_int, a_frac); } } else if (memcmp(config, "af ", 3) == 0) { - _prefs->airtime_factor = atof(&config[3]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "int.thresh ", 11) == 0) { - _prefs->interference_threshold = atoi(&config[11]); - 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, "multi.acks ", 11) == 0) { - _prefs->multi_acks = atoi(&config[11]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "allow.read.only ", 16) == 0) { - _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0; - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "flood.advert.interval ", 22) == 0) { - int hours = _atoi(&config[22]); - if ((hours > 0 && hours < 3) || (hours > 168)) { - strcpy(reply, "Error: interval range is 3-168 hours"); - } else { - _prefs->flood_advert_interval = (uint8_t)(hours); - _callbacks->updateFloodAdvertTimer(); - savePrefs(); - strcpy(reply, "OK"); - } - } else if (memcmp(config, "advert.interval ", 16) == 0) { - int mins = _atoi(&config[16]); - if ((mins > 0 && mins < MIN_LOCAL_ADVERT_INTERVAL) || (mins > 240)) { - sprintf(reply, "Error: interval range is %d-240 minutes", MIN_LOCAL_ADVERT_INTERVAL); - } else { - _prefs->advert_interval = (uint8_t)(mins / 2); - _callbacks->updateAdvertTimer(); - savePrefs(); - strcpy(reply, "OK"); - } - } else if (memcmp(config, "guest.password ", 15) == 0) { - StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password)); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "prv.key ", 8) == 0) { - uint8_t prv_key[PRV_KEY_SIZE]; - bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]); - // only allow rekey if key is valid - if (success && mesh::LocalIdentity::validatePrivateKey(prv_key)) { - mesh::LocalIdentity new_id; - new_id.readFrom(prv_key, PRV_KEY_SIZE); - _callbacks->saveIdentity(new_id); - strcpy(reply, "OK, reboot to apply! New pubkey: "); - mesh::Utils::toHex(&reply[33], new_id.pub_key, PUB_KEY_SIZE); - } else { - strcpy(reply, "Error, bad key"); - } - } else if (memcmp(config, "name ", 5) == 0) { - if (isValidName(&config[5])) { - StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name)); - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, bad chars"); - } - } else if (memcmp(config, "repeat ", 7) == 0) { - _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0; - savePrefs(); - strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); + _prefs->airtime_factor = atof(&config[3]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "int.thresh ", 11) == 0) { + _prefs->interference_threshold = atoi(&config[11]); + 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, "multi.acks ", 11) == 0) { + _prefs->multi_acks = atoi(&config[11]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "allow.read.only ", 16) == 0) { + _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "flood.advert.interval ", 22) == 0) { + int hours = _atoi(&config[22]); + if ((hours > 0 && hours < 3) || (hours > 168)) { + strcpy(reply, "Error: interval range is 3-168 hours"); + } else { + _prefs->flood_advert_interval = (uint8_t)(hours); + _callbacks->updateFloodAdvertTimer(); + savePrefs(); + strcpy(reply, "OK"); + } + } else if (memcmp(config, "advert.interval ", 16) == 0) { + int mins = _atoi(&config[16]); + if ((mins > 0 && mins < MIN_LOCAL_ADVERT_INTERVAL) || (mins > 240)) { + sprintf(reply, "Error: interval range is %d-240 minutes", MIN_LOCAL_ADVERT_INTERVAL); + } else { + _prefs->advert_interval = (uint8_t)(mins / 2); + _callbacks->updateAdvertTimer(); + savePrefs(); + strcpy(reply, "OK"); + } + } else if (memcmp(config, "guest.password ", 15) == 0) { + StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password)); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "prv.key ", 8) == 0) { + uint8_t prv_key[PRV_KEY_SIZE]; + bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]); + // only allow rekey if key is valid + if (success && mesh::LocalIdentity::validatePrivateKey(prv_key)) { + mesh::LocalIdentity new_id; + new_id.readFrom(prv_key, PRV_KEY_SIZE); + _callbacks->saveIdentity(new_id); + strcpy(reply, "OK, reboot to apply! New pubkey: "); + mesh::Utils::toHex(&reply[33], new_id.pub_key, PUB_KEY_SIZE); + } else { + strcpy(reply, "Error, bad key"); + } + } else if (memcmp(config, "name ", 5) == 0) { + if (isValidName(&config[5])) { + StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name)); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, bad chars"); + } + } else if (memcmp(config, "repeat ", 7) == 0) { + _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0; + savePrefs(); + strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); #if defined(USE_SX1262) || defined(USE_SX1268) } else if (memcmp(config, "radio.rxgain ", 13) == 0) { - _prefs->rx_boosted_gain = memcmp(&config[13], "on", 2) == 0; - strcpy(reply, "OK"); - savePrefs(); - _callbacks->setRxBoostedGain(_prefs->rx_boosted_gain); + _prefs->rx_boosted_gain = memcmp(&config[13], "on", 2) == 0; + strcpy(reply, "OK"); + savePrefs(); + _callbacks->setRxBoostedGain(_prefs->rx_boosted_gain); #endif - } 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 = num > 0 ? strtof(parts[0], nullptr) : 0.0f; - float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f; - uint8_t sf = num > 2 ? atoi(parts[2]) : 0; - uint8_t cr = num > 3 ? atoi(parts[3]) : 0; + } else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) { + if (!_board->canControlLoRaFemLna()) { + strcpy(reply, "Error: unsupported by this board"); + } else if (memcmp(&config[17], "on", 2) == 0) { + if (_board->setLoRaFemLnaEnabled(true)) { + _prefs->radio_fem_rxgain = 1; + savePrefs(); + strcpy(reply, "OK - LoRa FEM RX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else if (memcmp(&config[17], "off", 3) == 0) { + if (_board->setLoRaFemLnaEnabled(false)) { + _prefs->radio_fem_rxgain = 0; + savePrefs(); + strcpy(reply, "OK - LoRa FEM RX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + } 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 = num > 0 ? strtof(parts[0], nullptr) : 0.0f; + float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f; + uint8_t sf = num > 2 ? atoi(parts[2]) : 0; + uint8_t cr = num > 3 ? atoi(parts[3]) : 0; if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) { - _prefs->sf = sf; - _prefs->cr = cr; - _prefs->freq = freq; - _prefs->bw = bw; - _callbacks->savePrefs(); - strcpy(reply, "OK - reboot to apply"); - } else { - strcpy(reply, "Error, invalid radio params"); - } - } else if (memcmp(config, "lat ", 4) == 0) { - _prefs->node_lat = atof(&config[4]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "lon ", 4) == 0) { - _prefs->node_lon = atof(&config[4]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "rxdelay ", 8) == 0) { - float db = atof(&config[8]); - if (db >= 0) { - _prefs->rx_delay_base = db; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, cannot be negative"); - } - } else if (memcmp(config, "txdelay ", 8) == 0) { - float f = atof(&config[8]); - if (f >= 0) { - _prefs->tx_delay_factor = f; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, cannot be negative"); - } - } else if (memcmp(config, "flood.max ", 10) == 0) { - uint8_t m = atoi(&config[10]); - if (m <= 64) { - _prefs->flood_max = m; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, max 64"); - } - } else if (memcmp(config, "direct.txdelay ", 15) == 0) { - float f = atof(&config[15]); - if (f >= 0) { - _prefs->direct_tx_delay_factor = f; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, cannot be negative"); - } - } else if (memcmp(config, "owner.info ", 11) == 0) { - config += 11; - char *dp = _prefs->owner_info; - while (*config && dp - _prefs->owner_info < sizeof(_prefs->owner_info)-1) { - *dp++ = (*config == '|') ? '\n' : *config; // translate '|' to newline chars - config++; - } - *dp = 0; - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "path.hash.mode ", 15) == 0) { - config += 15; - uint8_t mode = atoi(config); - if (mode < 3) { - _prefs->path_hash_mode = mode; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be 0,1, or 2"); - } - } else if (memcmp(config, "loop.detect ", 12) == 0) { - config += 12; - uint8_t mode; - if (memcmp(config, "off", 3) == 0) { - mode = LOOP_DETECT_OFF; - } else if (memcmp(config, "minimal", 7) == 0) { - mode = LOOP_DETECT_MINIMAL; - } else if (memcmp(config, "moderate", 8) == 0) { - mode = LOOP_DETECT_MODERATE; - } else if (memcmp(config, "strict", 6) == 0) { - mode = LOOP_DETECT_STRICT; - } else { - mode = 0xFF; - strcpy(reply, "Error, must be: off, minimal, moderate, or strict"); - } - if (mode != 0xFF) { - _prefs->loop_detect = mode; - savePrefs(); - strcpy(reply, "OK"); - } - } 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) { - _prefs->freq = atof(&config[5]); - savePrefs(); - strcpy(reply, "OK - reboot to apply"); + _prefs->sf = sf; + _prefs->cr = cr; + _prefs->freq = freq; + _prefs->bw = bw; + _callbacks->savePrefs(); + strcpy(reply, "OK - reboot to apply"); + } else { + strcpy(reply, "Error, invalid radio params"); + } + } else if (memcmp(config, "lat ", 4) == 0) { + _prefs->node_lat = atof(&config[4]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "lon ", 4) == 0) { + _prefs->node_lon = atof(&config[4]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "rxdelay ", 8) == 0) { + float db = atof(&config[8]); + if (db >= 0) { + _prefs->rx_delay_base = db; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, cannot be negative"); + } + } else if (memcmp(config, "txdelay ", 8) == 0) { + float f = atof(&config[8]); + if (f >= 0) { + _prefs->tx_delay_factor = f; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, cannot be negative"); + } + } else if (memcmp(config, "flood.max ", 10) == 0) { + uint8_t m = atoi(&config[10]); + if (m <= 64) { + _prefs->flood_max = m; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, max 64"); + } + } else if (memcmp(config, "direct.txdelay ", 15) == 0) { + float f = atof(&config[15]); + if (f >= 0) { + _prefs->direct_tx_delay_factor = f; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, cannot be negative"); + } + } else if (memcmp(config, "owner.info ", 11) == 0) { + config += 11; + char *dp = _prefs->owner_info; + while (*config && dp - _prefs->owner_info < sizeof(_prefs->owner_info)-1) { + *dp++ = (*config == '|') ? '\n' : *config; // translate '|' to newline chars + config++; + } + *dp = 0; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "path.hash.mode ", 15) == 0) { + config += 15; + uint8_t mode = atoi(config); + if (mode < 3) { + _prefs->path_hash_mode = mode; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0,1, or 2"); + } + } else if (memcmp(config, "loop.detect ", 12) == 0) { + config += 12; + uint8_t mode; + if (memcmp(config, "off", 3) == 0) { + mode = LOOP_DETECT_OFF; + } else if (memcmp(config, "minimal", 7) == 0) { + mode = LOOP_DETECT_MINIMAL; + } else if (memcmp(config, "moderate", 8) == 0) { + mode = LOOP_DETECT_MODERATE; + } else if (memcmp(config, "strict", 6) == 0) { + mode = LOOP_DETECT_STRICT; + } else { + mode = 0xFF; + strcpy(reply, "Error, must be: off, minimal, moderate, or strict"); + } + if (mode != 0xFF) { + _prefs->loop_detect = mode; + savePrefs(); + strcpy(reply, "OK"); + } + } 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) { + _prefs->freq = atof(&config[5]); + savePrefs(); + strcpy(reply, "OK - reboot to apply"); #ifdef WITH_BRIDGE - } else if (memcmp(config, "bridge.enabled ", 15) == 0) { - _prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 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) { - _prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0; - savePrefs(); - strcpy(reply, "OK"); + } else if (memcmp(config, "bridge.enabled ", 15) == 0) { + _prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 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) { + _prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0; + savePrefs(); + strcpy(reply, "OK"); #endif #ifdef WITH_RS232_BRIDGE - } else if (memcmp(config, "bridge.baud ", 12) == 0) { - uint32_t baud = atoi(&config[12]); - if (baud >= 9600 && baud <= BRIDGE_MAX_BAUD) { - _prefs->bridge_baud = (uint32_t)baud; - _callbacks->restartBridge(); - savePrefs(); - strcpy(reply, "OK"); - } else { - sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD); - } + } else if (memcmp(config, "bridge.baud ", 12) == 0) { + uint32_t baud = atoi(&config[12]); + if (baud >= 9600 && baud <= BRIDGE_MAX_BAUD) { + _prefs->bridge_baud = (uint32_t)baud; + _callbacks->restartBridge(); + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD); + } #endif #ifdef WITH_ESPNOW_BRIDGE - } else if (memcmp(config, "bridge.channel ", 15) == 0) { - int ch = atoi(&config[15]); - if (ch > 0 && ch < 15) { - _prefs->bridge_channel = (uint8_t)ch; - _callbacks->restartBridge(); - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error: channel must be between 1-14"); - } - } else if (memcmp(config, "bridge.secret ", 14) == 0) { - StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret)); - _callbacks->restartBridge(); - savePrefs(); - strcpy(reply, "OK"); + } else if (memcmp(config, "bridge.channel ", 15) == 0) { + int ch = atoi(&config[15]); + if (ch > 0 && ch < 15) { + _prefs->bridge_channel = (uint8_t)ch; + _callbacks->restartBridge(); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error: channel must be between 1-14"); + } + } else if (memcmp(config, "bridge.secret ", 14) == 0) { + StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret)); + _callbacks->restartBridge(); + savePrefs(); + strcpy(reply, "OK"); #endif - } else if (memcmp(config, "adc.multiplier ", 15) == 0) { - _prefs->adc_multiplier = atof(&config[15]); - if (_board->setAdcMultiplier(_prefs->adc_multiplier)) { - savePrefs(); - if (_prefs->adc_multiplier == 0.0f) { - strcpy(reply, "OK - using default board multiplier"); + } else if (memcmp(config, "adc.multiplier ", 15) == 0) { + _prefs->adc_multiplier = atof(&config[15]); + if (_board->setAdcMultiplier(_prefs->adc_multiplier)) { + savePrefs(); + if (_prefs->adc_multiplier == 0.0f) { + strcpy(reply, "OK - using default board multiplier"); + } else { + sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier); + } + } else { + _prefs->adc_multiplier = 0.0f; + strcpy(reply, "Error: unsupported by this board"); + }; } else { - sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier); + sprintf(reply, "unknown config: %s", config); } - } else { - _prefs->adc_multiplier = 0.0f; - strcpy(reply, "Error: unsupported by this board"); - }; - } else { - sprintf(reply, "unknown config: %s", config); - } } void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) { @@ -801,9 +868,9 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "> minimal"); } else if (_prefs->loop_detect == LOOP_DETECT_MODERATE) { strcpy(reply, "> moderate"); - } else { + } else { strcpy(reply, "> strict"); - } + } } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) { sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm); } else if (memcmp(config, "freq", 4) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index ffdc7c6536..828f89f0b1 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -59,6 +59,7 @@ struct NodePrefs { // persisted to file float adc_multiplier; char owner_info[120]; uint8_t rx_boosted_gain; // power settings + uint8_t radio_fem_rxgain; // LoRa FEM RX gain setting uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; }; diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index c2d78ae08f..6d527b6627 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -12,17 +12,19 @@ #include #include #include -#include "driver/rtc_io.h" +#include "soc/rtc.h" +#include "esp_system.h" class ESP32Board : public mesh::MainBoard { protected: uint8_t startup_reason; bool inhibit_sleep = false; + static inline portMUX_TYPE sleepMux = portMUX_INITIALIZER_UNLOCKED; public: void begin() { // for future use, sub-classes SHOULD call this from their begin() - startup_reason = BD_STARTUP_NORMAL; + startup_reason = BD_STARTUP_NORMAL; #ifdef ESP32_CPU_FREQ setCpuFrequencyMhz(ESP32_CPU_FREQ); @@ -45,7 +47,7 @@ class ESP32Board : public mesh::MainBoard { #endif #else Wire.begin(); - #endif + #endif } // Temperature from ESP32 MCU @@ -60,25 +62,60 @@ class ESP32Board : public mesh::MainBoard { return raw / 4; } - void enterLightSleep(uint32_t secs) { -#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(P_LORA_DIO_1) // Supported ESP32 variants - if (rtc_gpio_is_valid_gpio((gpio_num_t)P_LORA_DIO_1)) { // Only enter sleep mode if P_LORA_DIO_1 is RTC pin - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // To wake up when receiving a LoRa packet + uint32_t getIRQGpio() { + return P_LORA_DIO_1; // default for SX1262 + } + + void sleep(uint32_t secs) override { + // Skip if not allow to sleep + if (inhibit_sleep) { + delay(1); // Give MCU to OTA to run + return; + } + + // Use more accurate clock in sleep +#if SOC_RTC_SLOW_CLK_SUPPORT_RC_FAST_D256 + if (rtc_clk_slow_src_get() != SOC_RTC_SLOW_CLK_SRC_RC_FAST) { - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); // To wake up every hour to do periodically jobs - } + // Switch slow clock source to RC_FAST / 256 (~31.25 kHz) + rtc_clk_slow_src_set(SOC_RTC_SLOW_CLK_SRC_RC_FAST); - esp_light_sleep_start(); // CPU enters light sleep + // Calibrate slow clock + esp_clk_slow_boot_cal(1024); } #endif - } - void sleep(uint32_t secs) override { - if (!inhibit_sleep) { - enterLightSleep(secs); // To wake up after "secs" seconds or when receiving a LoRa packet + // Set GPIO wakeup + gpio_num_t wakeupPin = (gpio_num_t)getIRQGpio(); + + // Configure timer wakeup + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000ULL); // Wake up periodically to do scheduled jobs + } + + // Disable CPU interrupt servicing + portENTER_CRITICAL(&sleepMux); + + // Skip sleep if there is a LoRa packet + if (gpio_get_level(wakeupPin) == HIGH) { + portEXIT_CRITICAL(&sleepMux); + delay(1); + return; } + + // Configure GPIO wakeup + esp_sleep_enable_gpio_wakeup(); + gpio_wakeup_enable((gpio_num_t)wakeupPin, GPIO_INTR_HIGH_LEVEL); // Wake up when receiving a LoRa packet + + // MCU enters light sleep + esp_light_sleep_start(); + + // Avoid ISR flood during wakeup due to HIGH LEVEL interrupt + gpio_wakeup_disable(wakeupPin); + gpio_set_intr_type(wakeupPin, GPIO_INTR_POSEDGE); + + // Enable CPU interrupt servicing + portEXIT_CRITICAL(&sleepMux); } uint8_t getStartupReason() const override { return startup_reason; } @@ -102,7 +139,7 @@ class ESP32Board : public mesh::MainBoard { #endif uint16_t getBattMilliVolts() override { - #ifdef PIN_VBAT_READ + #ifdef PIN_VBAT_READ analogReadResolution(12); uint32_t raw = 0; @@ -130,31 +167,88 @@ class ESP32Board : public mesh::MainBoard { void setInhibitSleep(bool inhibit) { inhibit_sleep = inhibit; } + + uint32_t getResetReason() const override { + return esp_reset_reason(); + } + + // https://docs.espressif.com/projects/esp-idf/en/v4.4.7/esp32/api-reference/system/system.html + const char *getResetReasonString(uint32_t reason) { + switch (reason) { + case ESP_RST_UNKNOWN: + return "Unknown or first boot"; + case ESP_RST_POWERON: + return "Power-on reset"; + case ESP_RST_EXT: + return "External reset"; + case ESP_RST_SW: + return "Software reset"; + case ESP_RST_PANIC: + return "Panic / exception reset"; + case ESP_RST_INT_WDT: + return "Interrupt watchdog reset"; + case ESP_RST_TASK_WDT: + return "Task watchdog reset"; + case ESP_RST_WDT: + return "Other watchdog reset"; + case ESP_RST_DEEPSLEEP: + return "Wake from deep sleep"; + case ESP_RST_BROWNOUT: + return "Brownout (low voltage)"; + case ESP_RST_SDIO: + return "SDIO reset"; + default: + static char buf[40]; + snprintf(buf, sizeof(buf), "Unknown reset reason (%d)", reason); + return buf; + } + } }; +static RTC_NOINIT_ATTR uint32_t _rtc_backup_time; +static RTC_NOINIT_ATTR uint32_t _rtc_backup_magic; +#define RTC_BACKUP_MAGIC 0xAA55CC33 +#define RTC_TIME_MIN 1772323200 // 1 Mar 2026 + class ESP32RTCClock : public mesh::RTCClock { public: ESP32RTCClock() { } void begin() { esp_reset_reason_t reason = esp_reset_reason(); - if (reason == ESP_RST_POWERON) { - // start with some date/time in the recent past - struct timeval tv; - tv.tv_sec = 1715770351; // 15 May 2024, 8:50pm - tv.tv_usec = 0; - settimeofday(&tv, NULL); + if (reason == ESP_RST_DEEPSLEEP) { + return; // ESP-IDF preserves system time across deep sleep + } + // All other resets (power-on, crash, WDT, brownout) lose system time. + // Restore from RTC backup if valid, otherwise use hardcoded seed. + struct timeval tv; + if (_rtc_backup_magic == RTC_BACKUP_MAGIC && _rtc_backup_time > RTC_TIME_MIN) { + tv.tv_sec = _rtc_backup_time; + } else { + tv.tv_sec = 1772323200; // 1 Mar 2026 } + tv.tv_usec = 0; + settimeofday(&tv, NULL); } uint32_t getCurrentTime() override { time_t _now; time(&_now); return _now; } - void setCurrentTime(uint32_t time) override { + void setCurrentTime(uint32_t time) override { struct timeval tv; tv.tv_sec = time; tv.tv_usec = 0; settimeofday(&tv, NULL); + _rtc_backup_time = time; + _rtc_backup_magic = RTC_BACKUP_MAGIC; + } + void tick() override { + time_t now; + time(&now); + if (now > RTC_TIME_MIN && (uint32_t)now != _rtc_backup_time) { + _rtc_backup_time = (uint32_t)now; + _rtc_backup_magic = RTC_BACKUP_MAGIC; + } } }; diff --git a/src/helpers/SensorManager.h b/src/helpers/SensorManager.h index 89a174c228..d4aa63b70f 100644 --- a/src/helpers/SensorManager.h +++ b/src/helpers/SensorManager.h @@ -2,6 +2,7 @@ #include #include "sensors/LocationProvider.h" +#include #define TELEM_PERM_BASE 0x01 // 'base' permission includes battery #define TELEM_PERM_LOCATION 0x02 @@ -15,6 +16,7 @@ class SensorManager { double node_altitude; // altitude in meters SensorManager() { node_lat = 0; node_lon = 0; node_altitude = 0; } + virtual bool i2c_probe(TwoWire& wire, uint8_t addr) { return false; } virtual bool begin() { return false; } virtual bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { return false; } virtual void loop() { } diff --git a/src/helpers/esp32/TBeamBoard.h b/src/helpers/esp32/TBeamBoard.h index 4ff9555103..98bd16bff4 100644 --- a/src/helpers/esp32/TBeamBoard.h +++ b/src/helpers/esp32/TBeamBoard.h @@ -59,13 +59,13 @@ // uint32_t P_LORA_BUSY = 0; //shared, so define at run // uint32_t P_LORA_DIO_2 = 0; //SX1276 only, so define at run - #define P_LORA_DIO_0 26 - #define P_LORA_DIO_1 33 - #define P_LORA_NSS 18 - #define P_LORA_RESET 23 - #define P_LORA_SCLK 5 - #define P_LORA_MISO 19 - #define P_LORA_MOSI 27 + // #define P_LORA_DIO_0 26 + // #define P_LORA_DIO_1 33 + // #define P_LORA_NSS 18 + // #define P_LORA_RESET 23 + // #define P_LORA_SCLK 5 + // #define P_LORA_MISO 19 + // #define P_LORA_MOSI 27 // #define PIN_GPS_RX 34 // #define PIN_GPS_TX 12 diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 19472406d8..749ea68971 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -7,8 +7,10 @@ #endif #ifdef ENV_INCLUDE_BME680 -#ifndef TELEM_BME680_ADDRESS -#define TELEM_BME680_ADDRESS 0x76 +#if defined(TELEM_BME680_ADDRESS) +uint8_t TELEM_BME680_ADDRESSES[] = { TELEM_BME680_ADDRESS }; +#else +uint8_t TELEM_BME680_ADDRESSES[] = { 0x76, 0x77 }; // Known I2C addresses for BME680 #endif #define TELEM_BME680_SEALEVELPRESSURE_HPA (1013.25) #include @@ -28,8 +30,10 @@ static Adafruit_AHTX0 AHTX0; #endif #if ENV_INCLUDE_BME280 -#ifndef TELEM_BME280_ADDRESS -#define TELEM_BME280_ADDRESS 0x76 // BME280 environmental sensor I2C address +#if defined(TELEM_BME280_ADDRESS) +uint8_t TELEM_BME280_ADDRESSES[] = { TELEM_BME280_ADDRESS }; +#else +uint8_t TELEM_BME280_ADDRESSES[] = { 0x76, 0x77 }; // Known I2C addresses for BME280 #endif #define TELEM_BME280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level #include @@ -37,8 +41,10 @@ static Adafruit_BME280 BME280; #endif #if ENV_INCLUDE_BMP280 -#ifndef TELEM_BMP280_ADDRESS -#define TELEM_BMP280_ADDRESS 0x76 // BMP280 environmental sensor I2C address +#if defined(TELEM_BMP280_ADDRESS) +uint8_t TELEM_BMP280_ADDRESSES[] = { TELEM_BMP280_ADDRESS }; +#else +uint8_t TELEM_BMP280_ADDRESSES[] = { 0x76, 0x77 }; // Known I2C addresses for BMP280 #endif #define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level #include @@ -161,6 +167,12 @@ class RAK12500LocationProvider : public LocationProvider { static RAK12500LocationProvider RAK12500_provider; #endif +bool EnvironmentSensorManager::i2c_probe(TwoWire &wire, uint8_t addr) { + wire.beginTransmission(addr); + uint8_t error = wire.endTransmission(); + return (error == 0); // If found i2c device, the error is 0 +} + bool EnvironmentSensorManager::begin() { #if ENV_INCLUDE_GPS #ifdef RAK_WISBLOCK_GPS @@ -182,7 +194,7 @@ bool EnvironmentSensorManager::begin() { #endif #if ENV_INCLUDE_AHTX0 - if (AHTX0.begin(TELEM_WIRE, 0, TELEM_AHTX_ADDRESS)) { + if (i2c_probe(*TELEM_WIRE, TELEM_AHTX_ADDRESS) && AHTX0.begin(TELEM_WIRE, 0, TELEM_AHTX_ADDRESS)) { MESH_DEBUG_PRINTLN("Found AHT10/AHT20 at address: %02X", TELEM_AHTX_ADDRESS); AHTX0_initialized = true; } else { @@ -192,46 +204,55 @@ bool EnvironmentSensorManager::begin() { #endif #if ENV_INCLUDE_BME680 - if (BME680.begin(TELEM_BME680_ADDRESS)) { - MESH_DEBUG_PRINTLN("Found BME680 at address: %02X", TELEM_BME680_ADDRESS); - BME680_initialized = true; - } else { - BME680_initialized = false; - MESH_DEBUG_PRINTLN("BME680 was not found at I2C address %02X", TELEM_BME680_ADDRESS); + for (size_t i = 0; i < sizeof(TELEM_BME680_ADDRESSES) / sizeof(TELEM_BME680_ADDRESSES[0]); i++) { + if (i2c_probe(*TELEM_WIRE, TELEM_BME680_ADDRESSES[i]) && BME680.begin(TELEM_BME680_ADDRESSES[i], TELEM_WIRE)) { + MESH_DEBUG_PRINTLN("Found BME680 at address: %02X", TELEM_BME680_ADDRESSES[i]); + BME680_initialized = true; + break; + } else { + BME680_initialized = false; + MESH_DEBUG_PRINTLN("BME680 was not found at I2C address %02X", TELEM_BME680_ADDRESSES[i]); + } } #endif #if ENV_INCLUDE_BME280 - if (BME280.begin(TELEM_BME280_ADDRESS, TELEM_WIRE)) { - MESH_DEBUG_PRINTLN("Found BME280 at address: %02X", TELEM_BME280_ADDRESS); - MESH_DEBUG_PRINTLN("BME sensor ID: %02X", BME280.sensorID()); - // Reduce self-heating: single-shot conversions, light oversampling, long standby. - BME280.setSampling(Adafruit_BME280::MODE_FORCED, + for (size_t i = 0; i < sizeof(TELEM_BME280_ADDRESSES) / sizeof(TELEM_BME280_ADDRESSES[0]); i++) { + if (i2c_probe(*TELEM_WIRE, TELEM_BME280_ADDRESSES[i]) && BME280.begin(TELEM_BME280_ADDRESSES[i], TELEM_WIRE)) { + MESH_DEBUG_PRINTLN("Found BME280 at address: %02X", TELEM_BME280_ADDRESSES[i]); + MESH_DEBUG_PRINTLN("BME sensor ID: %02X", BME280.sensorID()); + // Reduce self-heating: single-shot conversions, light oversampling, long standby. + BME280.setSampling(Adafruit_BME280::MODE_FORCED, Adafruit_BME280::SAMPLING_X1, // temperature Adafruit_BME280::SAMPLING_X1, // pressure Adafruit_BME280::SAMPLING_X1, // humidity Adafruit_BME280::FILTER_OFF, Adafruit_BME280::STANDBY_MS_1000); - BME280_initialized = true; - } else { - BME280_initialized = false; - MESH_DEBUG_PRINTLN("BME280 was not found at I2C address %02X", TELEM_BME280_ADDRESS); + BME280_initialized = true; + break; + } else { + BME280_initialized = false; + MESH_DEBUG_PRINTLN("BME280 was not found at I2C address %02X", TELEM_BME280_ADDRESSES[i]); + } } - #endif +#endif #if ENV_INCLUDE_BMP280 - if (BMP280.begin(TELEM_BMP280_ADDRESS)) { - MESH_DEBUG_PRINTLN("Found BMP280 at address: %02X", TELEM_BMP280_ADDRESS); - MESH_DEBUG_PRINTLN("BMP sensor ID: %02X", BMP280.sensorID()); - BMP280_initialized = true; - } else { - BMP280_initialized = false; - MESH_DEBUG_PRINTLN("BMP280 was not found at I2C address %02X", TELEM_BMP280_ADDRESS); - } - #endif + for (size_t i = 0; i < sizeof(TELEM_BMP280_ADDRESSES) / sizeof(TELEM_BMP280_ADDRESSES[0]); i++) { + if (i2c_probe(*TELEM_WIRE, TELEM_BMP280_ADDRESSES[i]) && BMP280.begin(TELEM_BMP280_ADDRESSES[i])) { + MESH_DEBUG_PRINTLN("Found BMP280 at address: %02X", TELEM_BMP280_ADDRESSES[i]); + MESH_DEBUG_PRINTLN("BMP sensor ID: %02X", BMP280.sensorID()); + BMP280_initialized = true; + break; + } else { + BMP280_initialized = false; + MESH_DEBUG_PRINTLN("BMP280 was not found at I2C address %02X", TELEM_BMP280_ADDRESSES[i]); + } + } +#endif #if ENV_INCLUDE_SHTC3 - if (SHTC3.begin(TELEM_WIRE)) { + if (i2c_probe(*TELEM_WIRE, 0x70) && SHTC3.begin(TELEM_WIRE)) { MESH_DEBUG_PRINTLN("Found sensor: SHTC3"); SHTC3_initialized = true; } else { @@ -242,21 +263,23 @@ bool EnvironmentSensorManager::begin() { #if ENV_INCLUDE_SHT4X - SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS); - uint32_t serialNumber = 0; - int16_t sht4x_error; - sht4x_error = SHT4X.serialNumber(serialNumber); - if (sht4x_error == 0) { - MESH_DEBUG_PRINTLN("Found SHT4X at address: %02X", TELEM_SHT4X_ADDRESS); - SHT4X_initialized = true; - } else { - SHT4X_initialized = false; - MESH_DEBUG_PRINTLN("SHT4X was not found at I2C address %02X", TELEM_SHT4X_ADDRESS); + if (i2c_probe(*TELEM_WIRE, TELEM_SHT4X_ADDRESS)) { + SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS); + uint32_t serialNumber = 0; + int16_t sht4x_error; + sht4x_error = SHT4X.serialNumber(serialNumber); + if (sht4x_error == 0) { + MESH_DEBUG_PRINTLN("Found SHT4X at address: %02X", TELEM_SHT4X_ADDRESS); + SHT4X_initialized = true; + } else { + SHT4X_initialized = false; + MESH_DEBUG_PRINTLN("SHT4X was not found at I2C address %02X", TELEM_SHT4X_ADDRESS); + } } #endif #if ENV_INCLUDE_LPS22HB - if (LPS22HB.begin()) { + if (i2c_probe(*TELEM_WIRE, 0x5C) && LPS22HB.begin()) { MESH_DEBUG_PRINTLN("Found sensor: LPS22HB"); LPS22HB_initialized = true; } else { @@ -266,7 +289,7 @@ bool EnvironmentSensorManager::begin() { #endif #if ENV_INCLUDE_INA3221 - if (INA3221.begin(TELEM_INA3221_ADDRESS, TELEM_WIRE)) { + if (i2c_probe(*TELEM_WIRE, TELEM_INA3221_ADDRESS) && INA3221.begin(TELEM_INA3221_ADDRESS, TELEM_WIRE)) { MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS); MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID()); @@ -281,7 +304,7 @@ bool EnvironmentSensorManager::begin() { #endif #if ENV_INCLUDE_INA219 - if (INA219.begin(TELEM_WIRE)) { + if (i2c_probe(*TELEM_WIRE, TELEM_INA219_ADDRESS) && INA219.begin(TELEM_WIRE)) { MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS); INA219_initialized = true; } else { @@ -291,17 +314,17 @@ bool EnvironmentSensorManager::begin() { #endif #if ENV_INCLUDE_INA260 - if (INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) { + if (i2c_probe(*TELEM_WIRE, TELEM_INA260_ADDRESS) && INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) { MESH_DEBUG_PRINTLN("Found INA260 at address: %02X", TELEM_INA260_ADDRESS); INA260_initialized = true; } else { INA260_initialized = false; - MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA260_ADDRESS); + MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA219_ADDRESS); } #endif #if ENV_INCLUDE_INA226 - if (INA226.begin()) { + if (i2c_probe(*TELEM_WIRE, TELEM_INA226_ADDRESS) && INA226.begin()) { MESH_DEBUG_PRINTLN("Found INA226 at address: %02X", TELEM_INA226_ADDRESS); INA226.setMaxCurrentShunt(TELEM_INA226_MAX_AMP, TELEM_INA226_SHUNT_VALUE); INA226_initialized = true; @@ -312,7 +335,7 @@ bool EnvironmentSensorManager::begin() { #endif #if ENV_INCLUDE_MLX90614 - if (MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) { + if (i2c_probe(*TELEM_WIRE, TELEM_MLX90614_ADDRESS) && MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) { MESH_DEBUG_PRINTLN("Found MLX90614 at address: %02X", TELEM_MLX90614_ADDRESS); MLX90614_initialized = true; } else { @@ -322,7 +345,7 @@ bool EnvironmentSensorManager::begin() { #endif #if ENV_INCLUDE_VL53L0X - if (VL53L0X.begin(TELEM_VL53L0X_ADDRESS, false, TELEM_WIRE)) { + if (i2c_probe(*TELEM_WIRE, TELEM_VL53L0X_ADDRESS) && VL53L0X.begin(TELEM_VL53L0X_ADDRESS, false, TELEM_WIRE)) { MESH_DEBUG_PRINTLN("Found VL53L0X at address: %02X", TELEM_VL53L0X_ADDRESS); VL53L0X_initialized = true; } else { @@ -334,7 +357,7 @@ bool EnvironmentSensorManager::begin() { #if ENV_INCLUDE_BMP085 // First argument is MODE (aka oversampling) // choose ULTRALOWPOWER - if (BMP085.begin(0, TELEM_WIRE)) { + if (i2c_probe(*TELEM_WIRE, 0x77) && BMP085.begin(0, TELEM_WIRE)) { MESH_DEBUG_PRINTLN("Found sensor BMP085"); BMP085_initialized = true; } else { @@ -345,7 +368,7 @@ bool EnvironmentSensorManager::begin() { #if ENV_INCLUDE_RAK12035 RAK12035.setup(*TELEM_WIRE); - if (RAK12035.begin(TELEM_RAK12035_ADDRESS)) { + if (i2c_probe(*TELEM_WIRE, TELEM_RAK12035_ADDRESS) && RAK12035.begin(TELEM_RAK12035_ADDRESS)) { MESH_DEBUG_PRINTLN("Found sensor RAK12035 at address: %02X", TELEM_RAK12035_ADDRESS); RAK12035_initialized = true; } else { @@ -716,7 +739,7 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ gps_detected = true; return true; } - + pinMode(ioPin, INPUT); MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next"); return false; @@ -759,7 +782,7 @@ void EnvironmentSensorManager::loop() { #if ENV_INCLUDE_GPS if (gps_active) { - _location->loop(); + _location->loop(); } if (millis() > next_gps_update) { diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h index 32413ebc03..38de556068 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -47,6 +47,7 @@ class EnvironmentSensorManager : public SensorManager { #else EnvironmentSensorManager(){}; #endif + bool i2c_probe(TwoWire& wire, uint8_t addr) override; bool begin() override; bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override; #if ENV_INCLUDE_GPS diff --git a/variants/gat562_30s_mesh_kit/platformio.ini b/variants/gat562_30s_mesh_kit/platformio.ini index 1467f0fa3d..4266d1346b 100644 --- a/variants/gat562_30s_mesh_kit/platformio.ini +++ b/variants/gat562_30s_mesh_kit/platformio.ini @@ -7,7 +7,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/gat562_30s_mesh_kit -D RAK_4631 -D RAK_BOARD - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/gat562_mesh_tracker_pro/platformio.ini b/variants/gat562_mesh_tracker_pro/platformio.ini index 8a947bce74..cf25424b72 100644 --- a/variants/gat562_mesh_tracker_pro/platformio.ini +++ b/variants/gat562_mesh_tracker_pro/platformio.ini @@ -5,7 +5,7 @@ board_check = true build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_tracker_pro - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index b985030f79..8011641dbe 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -12,7 +12,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/heltec_t114 -I src/helpers/ui -D HELTEC_T114 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=20 -D P_LORA_NSS=24 -D P_LORA_RESET=25 diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp index aabfed7967..f182c905e6 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp @@ -82,3 +82,21 @@ void HeltecTrackerV2Board::begin() { const char* HeltecTrackerV2Board::getManufacturerName() const { return "Heltec Tracker V2"; } + + bool HeltecTrackerV2Board::setLoRaFemLnaEnabled(bool enable) { + if (!loRaFEMControl.isLnaCanControl()) { + return false; + } + + loRaFEMControl.setLNAEnable(enable); + loRaFEMControl.setRxModeEnable(); + return true; + } + + bool HeltecTrackerV2Board::canControlLoRaFemLna() const { + return loRaFEMControl.isLnaCanControl(); + } + + bool HeltecTrackerV2Board::isLoRaFemLnaEnabled() const { + return loRaFEMControl.isLNAEnabled(); + } diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h index 33c897bc94..ccbecc7ab6 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h @@ -21,5 +21,8 @@ class HeltecTrackerV2Board : public ESP32Board { void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; + bool setLoRaFemLnaEnabled(bool enable) override; + bool canControlLoRaFemLna() const override; + bool isLoRaFemLnaEnabled() const override; }; diff --git a/variants/heltec_tracker_v2/LoRaFEMControl.h b/variants/heltec_tracker_v2/LoRaFEMControl.h index 2c50b74289..0ce60fffd8 100644 --- a/variants/heltec_tracker_v2/LoRaFEMControl.h +++ b/variants/heltec_tracker_v2/LoRaFEMControl.h @@ -12,8 +12,9 @@ class LoRaFEMControl void setRxModeEnable(void); void setRxModeEnableWhenMCUSleep(void); void setLNAEnable(bool enabled); - bool isLnaCanControl(void) { return lna_can_control; } + bool isLnaCanControl(void) const { return lna_can_control; } void setLnaCanControl(bool can_control) { lna_can_control = can_control; } + bool isLNAEnabled(void) const { return lna_enabled; } private: bool lna_enabled = false; diff --git a/variants/heltec_v2/HeltecV2Board.h b/variants/heltec_v2/HeltecV2Board.h index a6221036dd..fe800890b8 100644 --- a/variants/heltec_v2/HeltecV2Board.h +++ b/variants/heltec_v2/HeltecV2Board.h @@ -17,12 +17,12 @@ class HeltecV2Board : public ESP32Board { esp_reset_reason_t reason = esp_reset_reason(); if (reason == ESP_RST_DEEPSLEEP) { long wakeup_source = esp_sleep_get_ext1_wakeup_status(); - if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep) + if (wakeup_source & (1 << P_LORA_DIO_0)) { // received a LoRa packet (while in deep sleep) startup_reason = BD_STARTUP_RX_PACKET; } rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS); - rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); + rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_0); } } @@ -30,15 +30,15 @@ class HeltecV2Board : public ESP32Board { esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_0, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_0); rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_0), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_0) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn } if (secs > 0) { @@ -64,4 +64,8 @@ class HeltecV2Board : public ESP32Board { const char* getManufacturerName() const override { return "Heltec V2"; } + + uint32_t getIRQGpio() override { + return P_LORA_DIO_0; // default for SX1276 + } }; diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index 99f6f7e13c..2ff6e9ff6b 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -7,10 +7,10 @@ build_flags = -D HELTEC_LORA_V2 -D RADIO_CLASS=CustomSX1276 -D WRAPPER_CLASS=CustomSX1276Wrapper - -D P_LORA_DIO_1=26 + -D P_LORA_DIO_0=26 + -D P_LORA_DIO_1=35 -D P_LORA_NSS=18 - -D P_LORA_RESET=RADIOLIB_NC - -D P_LORA_BUSY=RADIOLIB_NC + -D P_LORA_RESET=14 -D P_LORA_SCLK=5 -D P_LORA_MISO=19 -D P_LORA_MOSI=27 @@ -172,6 +172,31 @@ lib_deps = ${Heltec_lora32_v2.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_v2_companion_radio_ble_ps] +extends = Heltec_lora32_v2 +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${Heltec_lora32_v2.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=SSD1306Display + -D MAX_CONTACTS=160 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_lora32_v2.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_lora32_v2.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Heltec_v2_companion_radio_wifi] extends = Heltec_lora32_v2 build_flags = diff --git a/variants/heltec_v2/target.cpp b/variants/heltec_v2/target.cpp index 2dfb4c6e1a..54a2d7b278 100644 --- a/variants/heltec_v2/target.cpp +++ b/variants/heltec_v2/target.cpp @@ -5,9 +5,9 @@ HeltecV2Board board; #if defined(P_LORA_SCLK) static SPIClass spi; - RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi); #else - RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1); #endif WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 803ee683e0..6a62b24d0b 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -179,6 +179,32 @@ lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_v3_companion_radio_ble_ps] +extends = Heltec_lora32_v3 +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${Heltec_lora32_v3.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display + -D BLE_PIN_CODE=123456 ; dynamic, random PIN + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_lora32_v3.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Heltec_v3_companion_radio_wifi] extends = Heltec_lora32_v3 build_flags = diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index 49580d2ecf..4537276f3d 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -20,7 +20,7 @@ void HeltecV4Board::begin() { rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS); rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); } - } +} void HeltecV4Board::onBeforeTransmit(void) { digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on @@ -83,3 +83,25 @@ void HeltecV4Board::begin() { return loRaFEMControl.getFEMType() == KCT8103L_PA ? "Heltec V4.3 OLED" : "Heltec V4 OLED"; #endif } + + bool HeltecV4Board::setLoRaFemLnaEnabled(bool enable) { +#if defined(RADIO_FEM_RXGAIN) && (RADIO_FEM_RXGAIN == 0) + enable = false; +#endif + + if (!loRaFEMControl.isLnaCanControl()) { + return false; + } + + loRaFEMControl.setLNAEnable(enable); + loRaFEMControl.setRxModeEnable(); + return true; + } + + bool HeltecV4Board::canControlLoRaFemLna() const { + return loRaFEMControl.isLnaCanControl(); + } + + bool HeltecV4Board::isLoRaFemLnaEnabled() const { + return loRaFEMControl.isLNAEnabled(); + } diff --git a/variants/heltec_v4/HeltecV4Board.h b/variants/heltec_v4/HeltecV4Board.h index 4d5ee46155..fe77caed05 100644 --- a/variants/heltec_v4/HeltecV4Board.h +++ b/variants/heltec_v4/HeltecV4Board.h @@ -19,5 +19,8 @@ class HeltecV4Board : public ESP32Board { void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; + bool setLoRaFemLnaEnabled(bool enable) override; + bool canControlLoRaFemLna() const override; + bool isLoRaFemLnaEnabled() const override; }; diff --git a/variants/heltec_v4/LoRaFEMControl.h b/variants/heltec_v4/LoRaFEMControl.h index 7545296503..d84ebe9c6a 100644 --- a/variants/heltec_v4/LoRaFEMControl.h +++ b/variants/heltec_v4/LoRaFEMControl.h @@ -18,8 +18,9 @@ class LoRaFEMControl void setRxModeEnable(void); void setRxModeEnableWhenMCUSleep(void); void setLNAEnable(bool enabled); - bool isLnaCanControl(void) { return lna_can_control; } + bool isLnaCanControl(void) const { return lna_can_control; } void setLnaCanControl(bool can_control) { lna_can_control = can_control; } + bool isLNAEnabled(void) const { return lna_enabled; } LoRaFEMType getFEMType(void) const { return fem_type; } private: LoRaFEMType fem_type=OTHER_FEM_TYPES; diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index 6f6bf2b538..bc038c8e4c 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -102,6 +102,28 @@ lib_deps = ${esp32_ota.lib_deps} bakercp/CRC32 @ ^2.0.0 +[env:heltec_v4_expansionkit_repeater] +extends = heltec_v4_oled +build_flags = + ${heltec_v4_oled.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"Heltec Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D ENV_PIN_SDA=4 + -D ENV_PIN_SCL=3 +build_src_filter = ${heltec_v4_oled.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${heltec_v4_oled.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + [env:heltec_v4_repeater_bridge_espnow] extends = heltec_v4_oled build_flags = @@ -200,14 +222,66 @@ lib_deps = ${heltec_v4_oled.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:heltec_v4_companion_radio_wifi] +[env:heltec_v4_companion_radio_ble_ps] +extends = heltec_v4_oled +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${heltec_v4_oled.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display + -D BLE_PIN_CODE=123456 ; dynamic, random PIN + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${heltec_v4_oled.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_oled.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_3_companion_radio_ble_ps_femoff] extends = heltec_v4_oled +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 build_flags = ${heltec_v4_oled.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display + -D BLE_PIN_CODE=123456 ; dynamic, random PIN + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off +build_src_filter = ${heltec_v4_oled.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_oled.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_companion_radio_wifi] +extends = heltec_v4_oled +build_flags = + ${heltec_v4_oled.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display -D WIFI_DEBUG_LOGGING=1 -D WIFI_SSID='"myssid"' diff --git a/variants/lilygo_t3s3_sx1276/LilygoT3S3SX1276Board.h b/variants/lilygo_t3s3_sx1276/LilygoT3S3SX1276Board.h new file mode 100644 index 0000000000..7da620fd50 --- /dev/null +++ b/variants/lilygo_t3s3_sx1276/LilygoT3S3SX1276Board.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +class LilygoT3S3SX1276Board : public ESP32Board { +public: + uint32_t getIRQGpio() override { + return P_LORA_DIO_0; // default for SX1276 + } +}; \ No newline at end of file diff --git a/variants/lilygo_t3s3_sx1276/target.cpp b/variants/lilygo_t3s3_sx1276/target.cpp index e7fe07a0c4..8236f45960 100644 --- a/variants/lilygo_t3s3_sx1276/target.cpp +++ b/variants/lilygo_t3s3_sx1276/target.cpp @@ -1,7 +1,7 @@ #include #include "target.h" -ESP32Board board; +LilygoT3S3SX1276Board board; #if defined(P_LORA_SCLK) static SPIClass spi; diff --git a/variants/lilygo_t3s3_sx1276/target.h b/variants/lilygo_t3s3_sx1276/target.h index 2df4b3edb5..079d2a7ea2 100644 --- a/variants/lilygo_t3s3_sx1276/target.h +++ b/variants/lilygo_t3s3_sx1276/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include +#include #include #include #include @@ -12,7 +12,7 @@ #include #endif -extern ESP32Board board; +extern LilygoT3S3SX1276Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; extern SensorManager sensors; diff --git a/variants/lilygo_tbeam_SX1262/platformio.ini b/variants/lilygo_tbeam_SX1262/platformio.ini index d3bc7c9978..1585dd74d3 100644 --- a/variants/lilygo_tbeam_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_SX1262/platformio.ini @@ -5,6 +5,13 @@ build_flags = ${esp32_base.build_flags} -I variants/lilygo_tbeam_SX1262 -D TBEAM_SX1262 + -D P_LORA_DIO_0=26 + -D P_LORA_DIO_1=33 + -D P_LORA_NSS=18 + -D P_LORA_RESET=23 + -D P_LORA_SCLK=5 + -D P_LORA_MISO=19 + -D P_LORA_MOSI=27 -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 diff --git a/variants/lilygo_tbeam_SX1276/LilygoTBeamSX1276Board.h b/variants/lilygo_tbeam_SX1276/LilygoTBeamSX1276Board.h new file mode 100644 index 0000000000..afe106d042 --- /dev/null +++ b/variants/lilygo_tbeam_SX1276/LilygoTBeamSX1276Board.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +class LilygoTBeamSX1276Board : public TBeamBoard { +public: + uint32_t getIRQGpio() override { + return P_LORA_DIO_0; // default for SX1276 + } +}; \ No newline at end of file diff --git a/variants/lilygo_tbeam_SX1276/platformio.ini b/variants/lilygo_tbeam_SX1276/platformio.ini index 3562c40e94..7482ef7bd8 100644 --- a/variants/lilygo_tbeam_SX1276/platformio.ini +++ b/variants/lilygo_tbeam_SX1276/platformio.ini @@ -5,6 +5,13 @@ build_flags = ${esp32_base.build_flags} -I variants/lilygo_tbeam_SX1276 -D TBEAM_SX1276 + -D P_LORA_DIO_0=26 + -D P_LORA_DIO_1=33 + -D P_LORA_NSS=18 + -D P_LORA_RESET=23 + -D P_LORA_SCLK=5 + -D P_LORA_MISO=19 + -D P_LORA_MOSI=27 -D SX127X_CURRENT_LIMIT=120 -D RADIO_CLASS=CustomSX1276 -D WRAPPER_CLASS=CustomSX1276Wrapper diff --git a/variants/lilygo_tbeam_SX1276/target.cpp b/variants/lilygo_tbeam_SX1276/target.cpp index 495337b8e2..5481d67240 100644 --- a/variants/lilygo_tbeam_SX1276/target.cpp +++ b/variants/lilygo_tbeam_SX1276/target.cpp @@ -1,7 +1,7 @@ #include #include "target.h" -TBeamBoard board; +LilygoTBeamSX1276Board board; #if defined(P_LORA_SCLK) static SPIClass spi; diff --git a/variants/lilygo_tbeam_SX1276/target.h b/variants/lilygo_tbeam_SX1276/target.h index ad3856455e..abeaef4698 100644 --- a/variants/lilygo_tbeam_SX1276/target.h +++ b/variants/lilygo_tbeam_SX1276/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 //#include #include -#include +#include #include #include #include @@ -12,7 +12,7 @@ #include #endif -extern TBeamBoard board; +extern LilygoTBeamSX1276Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; extern EnvironmentSensorManager sensors; diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index ffee37a969..249e68713b 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -5,6 +5,13 @@ build_flags = ${esp32_base.build_flags} -I variants/lilygo_tbeam_supreme_SX1262 -D TBEAM_SUPREME_SX1262 + -D P_LORA_DIO_0=26 + -D P_LORA_DIO_1=33 + -D P_LORA_NSS=18 + -D P_LORA_RESET=23 + -D P_LORA_SCLK=5 + -D P_LORA_MISO=19 + -D P_LORA_MOSI=27 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 -D USE_SX1262 diff --git a/variants/lilygo_tlora_v2_1/LilyGoTLoraBoard.h b/variants/lilygo_tlora_v2_1/LilyGoTLoraBoard.h index 545219b2bc..f126f00688 100644 --- a/variants/lilygo_tlora_v2_1/LilyGoTLoraBoard.h +++ b/variants/lilygo_tlora_v2_1/LilyGoTLoraBoard.h @@ -21,4 +21,8 @@ class LilyGoTLoraBoard : public ESP32Board { return (2 * raw); } + + uint32_t getIRQGpio() override { + return P_LORA_DIO_0; // default for SX1276 + } }; \ No newline at end of file diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 3d2d4a3ec5..e12071149f 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -6,7 +6,7 @@ build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak3401 -D RAK_3401 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index ea7e49c355..a96374a8de 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -7,7 +7,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/rak4631 -D RAK_4631 -D RAK_BOARD - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_GPS_TX=PIN_SERIAL1_RX diff --git a/variants/sensecap_solar/platformio.ini b/variants/sensecap_solar/platformio.ini index aabbcf000b..6aeb509b3a 100644 --- a/variants/sensecap_solar/platformio.ini +++ b/variants/sensecap_solar/platformio.ini @@ -10,7 +10,7 @@ build_flags = ${nrf52_base.build_flags} -I src/helpers/nrf52 -D NRF52_PLATFORM=1 -D USE_SX1262 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D P_LORA_TX_LED=11 diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index c5254b46c9..244a39f157 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -90,6 +90,28 @@ lib_deps = ${esp32_ota.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Xiao_C3_companion_radio_ble_ps] +extends = Xiao_esp32_C3 +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_src_filter = ${Xiao_esp32_C3.build_src_filter} + +<../examples/companion_radio/*.cpp> + + +build_flags = + ${Xiao_esp32_C3.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 + ; -D BLE_DEBUG_LOGGING=1 + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +lib_deps = + ${Xiao_esp32_C3.lib_deps} + ${esp32_ota.lib_deps} + densaugeo/base64 @ ~1.4.0 +board_build.partitions = min_spiffs.csv ; get around 4mb flash limit + [env:Xiao_C3_companion_radio_usb] extends = Xiao_esp32_C3 build_src_filter = ${Xiao_esp32_C3.build_src_filter} diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index a085433688..f4d1b93e6b 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/xiao_nrf52 -UENV_INCLUDE_GPS -D NRF52_PLATFORM - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D XIAO_NRF52 -D USE_SX1262 -D RADIO_CLASS=CustomSX1262 diff --git a/variants/xiao_s3/XiaoS3Board.h b/variants/xiao_s3/XiaoS3Board.h new file mode 100644 index 0000000000..288fcf6262 --- /dev/null +++ b/variants/xiao_s3/XiaoS3Board.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +class XiaoS3Board : public ESP32Board { +public: + XiaoS3Board() { } + + const char* getManufacturerName() const override { + return "Xiao S3"; + } +}; diff --git a/variants/xiao_s3/platformio.ini b/variants/xiao_s3/platformio.ini new file mode 100644 index 0000000000..58bcc60b90 --- /dev/null +++ b/variants/xiao_s3/platformio.ini @@ -0,0 +1,180 @@ +[Xiao_S3] +extends = esp32_base +board = seeed_xiao_esp32s3 +board_check = true +board_build.mcu = esp32s3 +build_flags = ${esp32_base.build_flags} + ${sensor_base.build_flags} + -I variants/xiao_s3 + -UENV_INCLUDE_GPS + -D SEEED_XIAO_S3 + -D PIN_VBAT_READ=1 ; D0 + -D P_LORA_DIO_1=2 ; D1 + -D P_LORA_NSS=5 ; D4 + -D P_LORA_RESET=3 ; D2 + -D P_LORA_BUSY=4 ; D3 + -D P_LORA_SCLK=7 ; D8 + -D P_LORA_MISO=8 ; D0 + -D P_LORA_MOSI=9 ; D10 + -D PIN_USER_BTN=-1 ; NC + -D PIN_STATUS_LED=21 ; Orange user led, LOW=On + -D PIN_BOARD_SDA=D6 ; D6=43 + -D PIN_BOARD_SCL=D7 ; D7=44 + -D SX126X_RXEN=6 ; D5 + -D SX126X_TXEN=RADIOLIB_NC + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_RX_BOOSTED_GAIN=1 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/xiao_s3> + + +lib_deps = + ${esp32_base.lib_deps} + ${sensor_base.lib_deps} + +[env:Xiao_S3_repeater] +extends = Xiao_S3 +build_src_filter = ${Xiao_S3.build_src_filter} + +<../examples/simple_repeater/*.cpp> +build_flags = + ${Xiao_S3.build_flags} + -D ADVERT_NAME='"Xiao S3 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = + ${Xiao_S3.lib_deps} + ${esp32_ota.lib_deps} + +[env:Xiao_S3_repeater_bridge_espnow] +extends = Xiao_S3 +build_src_filter = ${Xiao_S3.build_src_filter} + + + +<../examples/simple_repeater/*.cpp> +build_flags = + ${Xiao_S3.build_flags} + -D ADVERT_NAME='"ESPNow Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_ESPNOW_BRIDGE=1 +; -D BRIDGE_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = + ${Xiao_S3.lib_deps} + ${esp32_ota.lib_deps} + +[env:Xiao_S3_room_server] +extends = Xiao_S3 +build_src_filter = ${Xiao_S3.build_src_filter} + +<../examples/simple_room_server> +build_flags = + ${Xiao_S3.build_flags} + -D ADVERT_NAME='"Xiao S3 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = + ${Xiao_S3.lib_deps} + ${esp32_ota.lib_deps} + +[env:Xiao_S3_companion_radio_ble] +extends = Xiao_S3 +build_flags = + ${Xiao_S3.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D DISPLAY_CLASS=SSD1306Display + -D OFFLINE_QUEUE_SIZE=256 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Xiao_S3.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Xiao_S3.lib_deps} + densaugeo/base64 @ ~1.4.0 + adafruit/Adafruit SSD1306 @ ^2.5.13 + +[env:Xiao_S3_companion_radio_ble_ps] +extends = Xiao_S3 +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${Xiao_S3.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D DISPLAY_CLASS=SSD1306Display + -D OFFLINE_QUEUE_SIZE=256 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Xiao_S3.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Xiao_S3.lib_deps} + densaugeo/base64 @ ~1.4.0 + adafruit/Adafruit SSD1306 @ ^2.5.13 + +[env:Xiao_S3_companion_radio_usb] +extends = Xiao_S3 +build_flags = + ${Xiao_S3.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display + -D OFFLINE_QUEUE_SIZE=256 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Xiao_S3.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Xiao_S3.lib_deps} + densaugeo/base64 @ ~1.4.0 + adafruit/Adafruit SSD1306 @ ^2.5.13 + +[env:Xiao_S3_sensor] +extends = Xiao_S3 +build_src_filter = ${Xiao_S3.build_src_filter} + +<../examples/simple_sensor> +build_flags = + ${Xiao_S3.build_flags} + -D ADVERT_NAME='"Xiao S3 Sensor"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = + ${Xiao_S3.lib_deps} + ${esp32_ota.lib_deps} \ No newline at end of file diff --git a/variants/xiao_s3/target.cpp b/variants/xiao_s3/target.cpp new file mode 100644 index 0000000000..014b25529d --- /dev/null +++ b/variants/xiao_s3/target.cpp @@ -0,0 +1,56 @@ +#include +#include "target.h" + +XiaoS3Board board; + +#if defined(P_LORA_SCLK) + static SPIClass spi; + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +#else + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +#endif + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + pinMode(21, INPUT); + pinMode(48, OUTPUT); + + #if defined(P_LORA_SCLK) + spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); + return radio.std_init(&spi); +#else + return radio.std_init(); +#endif +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(int8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/xiao_s3/target.h b/variants/xiao_s3/target.h new file mode 100644 index 0000000000..93b2786295 --- /dev/null +++ b/variants/xiao_s3/target.h @@ -0,0 +1,30 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include + #include +#endif +#include "XiaoS3Board.h" + +extern XiaoS3Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(int8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index 13d406792b..7fcebce2eb 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -173,6 +173,32 @@ lib_deps = densaugeo/base64 @ ~1.4.0 adafruit/Adafruit SSD1306 @ ^2.5.13 +[env:Xiao_S3_WIO_companion_radio_ble_ps] +extends = Xiao_S3_WIO +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${Xiao_S3_WIO.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D DISPLAY_CLASS=SSD1306Display + -D OFFLINE_QUEUE_SIZE=256 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Xiao_S3_WIO.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Xiao_S3_WIO.lib_deps} + densaugeo/base64 @ ~1.4.0 + adafruit/Adafruit SSD1306 @ ^2.5.13 + [env:Xiao_S3_WIO_companion_radio_serial] extends = Xiao_S3_WIO build_flags = From 0702065dd273ab5b703625425883932f61c689b9 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 24 Apr 2026 23:05:31 +0700 Subject: [PATCH 10/94] Delay sleep when BLE is active writing. --- examples/companion_radio/main.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 90903097a5..b4e8d386e2 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -266,7 +266,9 @@ void loop() { #if defined(NRF52_PLATFORM) board.sleep(0); // nrf ignores seconds param, sleeps whenever possible #else if defined(ESP32_PLATFORM) - vTaskDelay(pdMS_TO_TICKS(50)); // attempt to sleep + if (!serial_interface.isWriteBusy()) { // BLE is not busy + vTaskDelay(pdMS_TO_TICKS(50)); // attempt to sleep + } #endif } } From 756268e2ee46c992e662bcb0c0318de25d266783 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 24 Apr 2026 14:41:07 -0700 Subject: [PATCH 11/94] Fix issue with packet prefixes getting added to the table. --- docs/cli_commands.md | 6 +- examples/simple_repeater/MyMesh.cpp | 93 ++++++++++++++++++++++------- src/helpers/SimpleMeshTables.h | 24 ++++---- 3 files changed, 91 insertions(+), 32 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index e798cf0e86..b6c87a7f02 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -119,19 +119,23 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Usage:** - `get recent.repeater` - `get recent.repeater all` +- `get recent.repeater all ` - `get recent.repeater first ` +- `get recent.repeater first ` - `get recent.repeater last ` +- `get recent.repeater last ` - `set recent.repeater ` **Parameters:** - `prefix_hex`: 1-3 bytes of next-hop prefix (hex) - `snr_db`: SNR in dB (supports decimals; stored at x4 precision) - `count`: number of entries to print +- `offset`: zero-based row offset into the selected order **Notes:** - `set` is rejected when the prefix already exists in neighbors. - `all` prints oldest to newest; `first` prints the oldest N; `last` prints the newest N. -- Remote CLI replies include rows too, but may truncate when the packet payload limit is reached. +- Over LoRa remote CLI, replies are packet-size limited; use `offset` to page through all rows. --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 220f970421..907ce2c8da 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1372,7 +1372,11 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply auto* tables = (SimpleMeshTables*)getTables(); - if (is_set || (!is_get && *sub != 0 && strcmp(sub, "all") != 0 && strncmp(sub, "first ", 6) != 0 && strncmp(sub, "last ", 5) != 0)) { + bool is_all = (strcmp(sub, "all") == 0 || strncmp(sub, "all ", 4) == 0); + bool is_first = (strncmp(sub, "first ", 6) == 0); + bool is_last = (strncmp(sub, "last ", 5) == 0); + + if (is_set || (!is_get && *sub != 0 && !is_all && !is_first && !is_last)) { char* params = (char*) sub; char* arg_snr = strchr(params, ' '); if (arg_snr == NULL) { @@ -1409,62 +1413,111 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply mesh::Utils::toHex(hex, info->prefix, info->prefix_len); sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); } - } else if (strcmp(sub, "all") == 0 || strncmp(sub, "first ", 6) == 0 || strncmp(sub, "last ", 5) == 0) { + } else if (is_all || is_first || is_last) { int total = tables->getRecentRepeaterCount(); if (total <= 0) { strcpy(reply, "> none"); } else { bool newest_first = false; int limit = total; + int offset = 0; const char* mode = "all"; - if (strncmp(sub, "first ", 6) == 0 || strncmp(sub, "last ", 5) == 0) { - const char* nstr = sub + (sub[0] == 'f' ? 6 : 5); + + if (is_first || is_last) { + const char* nstr = sub + (is_first ? 6 : 5); while (*nstr == ' ') nstr++; if (*nstr == 0) { - strcpy(reply, "Err - usage: get recent.repeater first|last "); + strcpy(reply, "Err - usage: get recent.repeater first|last [offset]"); return; } + char* end_ptr = NULL; - long parsed = strtol(nstr, &end_ptr, 10); + long parsed_count = strtol(nstr, &end_ptr, 10); while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; - if (end_ptr == NULL || *end_ptr != 0 || parsed <= 0) { + if (end_ptr == NULL || parsed_count <= 0) { strcpy(reply, "Err - count must be > 0"); return; } - limit = (int)parsed; - if (sub[0] == 'l') { + + if (*end_ptr != 0) { + char* end_ptr2 = NULL; + long parsed_offset = strtol(end_ptr, &end_ptr2, 10); + while (end_ptr2 != NULL && *end_ptr2 == ' ') end_ptr2++; + if (end_ptr2 == NULL || *end_ptr2 != 0 || parsed_offset < 0) { + strcpy(reply, "Err - offset must be >= 0"); + return; + } + offset = (int)parsed_offset; + } + + limit = (int)parsed_count; + if (is_last) { newest_first = true; mode = "last"; } else { mode = "first"; } + } else if (strncmp(sub, "all ", 4) == 0) { + const char* arg = sub + 4; + while (*arg == ' ') arg++; + if (*arg == 0) { + strcpy(reply, "Err - usage: get recent.repeater all "); + return; + } + + char* end_ptr = NULL; + long parsed_a = strtol(arg, &end_ptr, 10); + while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; + if (end_ptr == NULL || parsed_a <= 0) { + strcpy(reply, "Err - count must be > 0"); + return; + } + + char* end_ptr2 = NULL; + long parsed_b = strtol(end_ptr, &end_ptr2, 10); + while (end_ptr2 != NULL && *end_ptr2 == ' ') end_ptr2++; + if (end_ptr2 == NULL || *end_ptr2 != 0 || parsed_b < 0) { + strcpy(reply, "Err - usage: get recent.repeater all "); + return; + } + limit = (int)parsed_a; + offset = (int)parsed_b; } - if (limit > total) { - limit = total; + + if (offset >= total) { + sprintf(reply, "> none (%s off=%d/%d)", mode, offset, total); + return; + } + + int available = total - offset; + if (limit > available) { + limit = available; } if (sender_timestamp == 0) { - Serial.printf("Recent repeater table (%s %d/%d):\n", mode, limit, total); + Serial.printf("Recent repeater table (%s %d/%d, off=%d):\n", mode, limit, total, offset); for (int i = 0; i < limit; i++) { - const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(i) : tables->getRecentRepeaterOldestByIdx(i); + int idx = offset + i; + const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(idx) : tables->getRecentRepeaterOldestByIdx(idx); if (info == NULL) { continue; } char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - Serial.printf("%02d: %s,%s\n", i + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + Serial.printf("%02d: %s,%s\n", idx + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); } - sprintf(reply, "> showing %d/%d (%s)", limit, total, mode); + sprintf(reply, "> %s off=%d n=%d/%d", mode, offset, limit, total); } else { // Remote CLI replies are packet-bound, so include as many rows as fit. - int written = snprintf(reply, 160, "> showing %d/%d (%s)", limit, total, mode); + int written = snprintf(reply, 160, "> %s off=%d n=%d/%d", mode, offset, limit, total); bool truncated = false; if (written < 0) { reply[0] = 0; written = 0; } for (int i = 0; i < limit; i++) { - const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(i) : tables->getRecentRepeaterOldestByIdx(i); + int idx = offset + i; + const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(idx) : tables->getRecentRepeaterOldestByIdx(idx); if (info == NULL) { continue; } @@ -1474,7 +1527,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - int n = snprintf(reply + written, 160 - written, "\n%02d:%s,%s", i + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + int n = snprintf(reply + written, 160 - written, "\n%02d:%s,%s", idx + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); if (n < 0 || n >= (160 - written)) { truncated = true; break; @@ -1482,12 +1535,12 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply written += n; } if (truncated && written < 156) { - snprintf(reply + written, 160 - written, "\n..."); + snprintf(reply + written, 160 - written, "\n... use offset"); } } } } else { - strcpy(reply, "Err - usage: get recent.repeater [all|first |last ]"); + strcpy(reply, "Err - usage: get recent.repeater [all|all |first [offset]|last [offset]]"); } } else if (memcmp(command, "discover.neighbors", 18) == 0) { const char* sub = command + 18; diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 705869ad35..539bd5b641 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -70,6 +70,19 @@ class SimpleMeshTables : public mesh::MeshTables { bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { // Learn repeater prefixes only from packet shapes that expose a trustworthy repeater ID. + // For flood traffic, the last path entry is the repeater we directly heard. + if (packet->isRouteFlood() && packet->getPathHashCount() > 0) { + prefix_len = packet->getPathHashSize(); + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + const uint8_t* last_hop = &packet->path[(packet->getPathHashCount() - 1) * packet->getPathHashSize()]; + memcpy(prefix, last_hop, prefix_len); + return true; + } + + // If there is no flood path to inspect, fall back to payload-derived identities. if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->payload_len >= PUB_KEY_SIZE) { memcpy(prefix, packet->payload, MAX_ROUTE_HASH_BYTES); prefix_len = MAX_ROUTE_HASH_BYTES; @@ -86,17 +99,6 @@ class SimpleMeshTables : public mesh::MeshTables { return true; } - if (packet->isRouteFlood() && packet->getPathHashCount() > 0) { - prefix_len = packet->getPathHashSize(); - if (prefix_len > MAX_ROUTE_HASH_BYTES) { - prefix_len = MAX_ROUTE_HASH_BYTES; - } - - const uint8_t* last_hop = &packet->path[(packet->getPathHashCount() - 1) * packet->getPathHashSize()]; - memcpy(prefix, last_hop, prefix_len); - return true; - } - return false; } From 0243ec41e8e71828afecb175346848c91071113d Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 24 Apr 2026 15:19:48 -0700 Subject: [PATCH 12/94] Round up the SNR vs replacement to get a weighted average. --- src/helpers/SimpleMeshTables.h | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 539bd5b641..effea219b5 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -68,6 +68,21 @@ class SimpleMeshTables : public mesh::MeshTables { return n > 0 && memcmp(a, b, n) == 0; } + int8_t avgSnrX4RoundUp(int8_t curr_snr_x4, int8_t new_snr_x4) const { + int16_t sum = (int16_t)curr_snr_x4 + (int16_t)new_snr_x4; + int16_t avg = sum / 2; // truncates toward zero + // "Round up" means ceil(), which only differs from truncation for positive odd sums. + if (sum > 0 && (sum & 1)) { + avg++; + } + if (avg > 127) { + avg = 127; + } else if (avg < -128) { + avg = -128; + } + return (int8_t)avg; + } + bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { // Learn repeater prefixes only from packet shapes that expose a trustworthy repeater ID. // For flood traffic, the last path entry is the repeater we directly heard. @@ -258,7 +273,7 @@ class SimpleMeshTables : public mesh::MeshTables { memcpy(existing.prefix, prefix, prefix_len); existing.prefix_len = prefix_len; } - existing.snr_x4 = snr_x4; + existing.snr_x4 = avgSnrX4RoundUp(existing.snr_x4, snr_x4); return true; } From a44137662eb92a142fb67894aa8b9dc521914b1a Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sat, 25 Apr 2026 23:37:57 +0700 Subject: [PATCH 13/94] Added TimeTrim and Kept RTC 8M during sleep --- src/helpers/ESP32Board.h | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index 6d527b6627..ee874ff06f 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -66,6 +66,50 @@ class ESP32Board : public mesh::MainBoard { return P_LORA_DIO_1; // default for SX1262 } + // 27 mins drift in 28 days and 40 mins = 669 ppm + const int64_t MICROSECONDS_PER_SECOND_DRIFT = 669; + const int64_t DRIFT_THRESHOLD_US = 60000000LL; // 1 minute in microseconds + + void applyTimeTrim() { + static int64_t last_trim_us = 0; + static int64_t accumulated_drift_us = 0; + + struct timeval tv; + gettimeofday(&tv, NULL); + int64_t now_us = (int64_t)tv.tv_sec * 1000000LL + tv.tv_usec; + + if (last_trim_us == 0) { + last_trim_us = now_us; + return; + } + + // Calculate elapsed time + int64_t elapsed_us = now_us - last_trim_us; + + // Add this interval's drift to accumulated drift + accumulated_drift_us += (elapsed_us * MICROSECONDS_PER_SECOND_DRIFT) / 1000000LL; + + // Only trim when accumulated drift exceeds threshold + if (accumulated_drift_us >= DRIFT_THRESHOLD_US) { + // Calculate full seconds to trim + uint32_t seconds_to_trim = (uint32_t)(accumulated_drift_us / 1000000LL); + + // Set back the trimmed time to RTC + tv.tv_sec -= (time_t)seconds_to_trim; + settimeofday(&tv, NULL); + + // Keep the fractional remainder (anything less than a full second) + accumulated_drift_us %= 1000000LL; + + // Save for next trim + gettimeofday(&tv, NULL); + last_trim_us = (int64_t)tv.tv_sec * 1000000LL + tv.tv_usec; + } else { + // Skip trimming + last_trim_us = now_us; + } + } + void sleep(uint32_t secs) override { // Skip if not allow to sleep if (inhibit_sleep) { @@ -85,6 +129,9 @@ class ESP32Board : public mesh::MainBoard { } #endif + // Keep RTC 8M during sleep + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC8M, ESP_PD_OPTION_ON); + // Set GPIO wakeup gpio_num_t wakeupPin = (gpio_num_t)getIRQGpio(); @@ -116,6 +163,9 @@ class ESP32Board : public mesh::MainBoard { // Enable CPU interrupt servicing portEXIT_CRITICAL(&sleepMux); + + // Apply the software trim to correct for RC drift + applyTimeTrim(); } uint8_t getStartupReason() const override { return startup_reason; } From aa400da9459f228969ce9914950c46f7e8d99ef4 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sat, 25 Apr 2026 23:38:38 +0700 Subject: [PATCH 14/94] Clear stale wakeup sources to avoid ghost wakeup --- variants/heltec_v3/HeltecV3Board.h | 4 ++++ variants/heltec_v4/HeltecV4Board.cpp | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/variants/heltec_v3/HeltecV3Board.h b/variants/heltec_v3/HeltecV3Board.h index ba22a7f2b9..5361d0293d 100644 --- a/variants/heltec_v3/HeltecV3Board.h +++ b/variants/heltec_v3/HeltecV3Board.h @@ -53,6 +53,10 @@ class HeltecV3Board : public ESP32Board { } void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { + // Clear stale wakeup sources to avoid ghost wakeup + // This is required when Power Management and automatic lightsleep are enabled + esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index 4537276f3d..f9dafb3b77 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -33,6 +33,10 @@ void HeltecV4Board::begin() { } void HeltecV4Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + // Clear stale wakeup sources to avoid ghost wakeup + // This is required when Power Management and automatic lightsleep are enabled + esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep From 5a45d54afefcbdaf18037eca50a462dc472e8b00 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sun, 26 Apr 2026 16:16:43 +0700 Subject: [PATCH 15/94] Added LilyGo_TBeam_1W_companion_radio_ble_ps --- variants/lilygo_tbeam_1w/platformio.ini | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index 7c8453077f..60b3291b74 100644 --- a/variants/lilygo_tbeam_1w/platformio.ini +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -146,6 +146,31 @@ lib_deps = ${LilyGo_TBeam_1W.lib_deps} densaugeo/base64 @ ~1.4.0 +; === LILYGO T-Beam 1W Companion Radio PS (BLE PS) === +[env:LilyGo_TBeam_1W_companion_radio_ble_ps] +extends = LilyGo_TBeam_1W +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${LilyGo_TBeam_1W.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 + -D PERSISTANT_GPS=1 + -D ENV_SKIP_GPS_DETECT=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TBeam_1W.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_TBeam_1W.lib_deps} + densaugeo/base64 @ ~1.4.0 + ; === LILYGO T-Beam 1W Companion Radio (WiFi) === [env:LilyGo_TBeam_1W_companion_radio_wifi] extends = LilyGo_TBeam_1W From 181ddb2268ec3ccd1032fc0a388e54e0bc40659d Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Mon, 27 Apr 2026 10:18:55 +0700 Subject: [PATCH 16/94] Added Heltec_WSL3_companion_radio_ble_ps --- variants/heltec_v3/platformio.ini | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 6a62b24d0b..fe9cd9264b 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -346,6 +346,26 @@ lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_WSL3_companion_radio_ble_ps] +extends = Heltec_lora32_v3 +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${Heltec_lora32_v3.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = + ${Heltec_lora32_v3.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Heltec_WSL3_companion_radio_usb] extends = Heltec_lora32_v3 build_flags = From fa9f3bd9350762a0da47beb9b350dd154f375e83 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Mon, 27 Apr 2026 11:07:36 +0700 Subject: [PATCH 17/94] Added Heltec_Wireless_Tracker_companion_radio_ble_ps --- variants/heltec_tracker/platformio.ini | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/variants/heltec_tracker/platformio.ini b/variants/heltec_tracker/platformio.ini index e0a8f5fab6..2c9155269b 100644 --- a/variants/heltec_tracker/platformio.ini +++ b/variants/heltec_tracker/platformio.ini @@ -99,6 +99,33 @@ lib_deps = ${Heltec_tracker_base.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_Wireless_Tracker_companion_radio_ble_ps] +extends = Heltec_tracker_base +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${Heltec_tracker_base.build_flags} + -I src/helpers/ui + -I examples/companion_radio/ui-new + -D DISPLAY_ROTATION=1 + -D DISPLAY_CLASS=ST7735Display + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 ; HWT will use display for pin + -D OFFLINE_QUEUE_SIZE=256 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_tracker_base.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> + + +lib_deps = + ${Heltec_tracker_base.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Heltec_Wireless_Tracker_repeater] extends = Heltec_tracker_base build_flags = From e608503aa02e9c0dfa3ed583b4154157f5f2d421 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Mon, 27 Apr 2026 12:15:46 +0700 Subject: [PATCH 18/94] Merged latest changes from "Add CLI control to LoRa's fem LNA" https://github.com/meshcore-dev/MeshCore/pull/2140 --- docs/cli_commands.md | 14 + examples/companion_radio/DataStore.cpp | 2 + examples/companion_radio/MyMesh.cpp | 12 +- examples/companion_radio/NodePrefs.h | 2 +- examples/simple_repeater/UITask.cpp | 3 +- examples/simple_room_server/UITask.cpp | 3 +- examples/simple_sensor/SensorMesh.cpp | 2 + examples/simple_sensor/UITask.cpp | 3 +- src/helpers/CommonCLI.cpp | 538 ++++++++++---------- variants/heltec_t096/LoRaFEMControl.h | 5 +- variants/heltec_t096/T096Board.cpp | 20 +- variants/heltec_t096/T096Board.h | 3 + variants/heltec_tracker_v2/LoRaFEMControl.h | 2 +- variants/heltec_v4/HeltecV4Board.cpp | 2 +- variants/heltec_v4/HeltecV4Board.h | 4 +- 15 files changed, 332 insertions(+), 283 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index fb698228ed..c17910a5d2 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -277,6 +277,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change the LoRa FEM receive-path gain state on supported boards +**Usage:** +- `get radio.fem.rxgain` +- `set radio.fem.rxgain ` + +**Parameters:** +- `state`: `on`|`off` + +**Notes:** +- This controls the external LoRa FEM receive-path LNA where the board supports it. +- This is separate from `radio.rxgain`, which controls the radio chip receive gain mode. + +--- + ### System #### View or change this node's name diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index c7988bb344..362ba68a33 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -233,6 +233,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 + file.read((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122 file.close(); } @@ -273,6 +274,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 + file.write((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122 file.close(); } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 67ff0a81ce..8a6f63c533 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -46,9 +46,7 @@ #define CMD_SET_CUSTOM_VAR 41 #define CMD_GET_ADVERT_PATH 42 #define CMD_GET_TUNING_PARAMS 43 -#define CMD_GET_RADIO_FEM_RXGAIN 44 -#define CMD_SET_RADIO_FEM_RXGAIN 45 -// NOTE: CMD range 46..49 parked, potentially for WiFi operations +// NOTE: CMD range 44..49 parked, potentially for WiFi operations #define CMD_SEND_BINARY_REQ 50 #define CMD_FACTORY_RESET 51 #define CMD_SEND_PATH_DISCOVERY_REQ 52 @@ -63,6 +61,8 @@ #define CMD_SEND_CHANNEL_DATA 62 #define CMD_SET_DEFAULT_FLOOD_SCOPE 63 #define CMD_GET_DEFAULT_FLOOD_SCOPE 64 +#define CMD_GET_RADIO_FEM_RXGAIN 65 +#define CMD_SET_RADIO_FEM_RXGAIN 66 // Stats sub-types for CMD_GET_STATS #define STATS_TYPE_CORE 0 @@ -1808,9 +1808,9 @@ void MyMesh::handleCmdFrame(size_t len) { writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); } else { out_frame[0] = RESP_CODE_OK; - uint32_t value = board.isLoRaFemLnaEnabled() ? 1 : 0; - memcpy(&out_frame[1], &value, 4); - _serial->writeFrame(out_frame, 5); + uint8_t value = board.isLoRaFemLnaEnabled() ? 1 : 0; + memcpy(&out_frame[1], &value, 1); + _serial->writeFrame(out_frame, 2); } } else if (cmd_frame[0] == CMD_SET_RADIO_FEM_RXGAIN && len >= 2) { uint8_t value = cmd_frame[1]; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 6598a69c60..ecb117bd2f 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -35,4 +35,4 @@ struct NodePrefs { // persisted to file uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) char default_scope_name[31]; uint8_t default_scope_key[16]; -}; \ No newline at end of file +}; diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index acb4632581..ac6958bae8 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -41,7 +41,8 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi } // v1.2.3 (1 Jan 2025) - sprintf(_version_info, "%s (%s)", version, build_date); + snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date); + free(version); } void UITask::renderCurrScreen() { diff --git a/examples/simple_room_server/UITask.cpp b/examples/simple_room_server/UITask.cpp index 42bc14d4a5..6445baf6f7 100644 --- a/examples/simple_room_server/UITask.cpp +++ b/examples/simple_room_server/UITask.cpp @@ -41,7 +41,8 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi } // v1.2.3 (1 Jan 2025) - sprintf(_version_info, "%s (%s)", version, build_date); + snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date); + free(version); } void UITask::renderCurrScreen() { diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index b8fe1e579c..05b4a9ddb5 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -731,6 +731,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise _prefs.gps_enabled = 0; _prefs.gps_interval = 0; _prefs.advert_loc_policy = ADVERT_LOC_PREFS; + _prefs.radio_fem_rxgain = 1; memset(default_scope.key, 0, sizeof(default_scope.key)); } @@ -766,6 +767,7 @@ void SensorMesh::begin(FILESYSTEM* fs) { radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_set_tx_power(_prefs.tx_power_dbm); + board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/examples/simple_sensor/UITask.cpp b/examples/simple_sensor/UITask.cpp index 0e78fee005..698b9fd2f2 100644 --- a/examples/simple_sensor/UITask.cpp +++ b/examples/simple_sensor/UITask.cpp @@ -41,7 +41,8 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi } // v1.2.3 (1 Jan 2025) - sprintf(_version_info, "%s (%s)", version, build_date); + snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date); + free(version); } void UITask::renderCurrScreen() { diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 1aa4265c23..1322a36cd1 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -90,7 +90,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 291 + // next: 292 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -182,7 +183,6 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 291 // next: 292 @@ -522,279 +522,279 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "OK - %d.%d%%", a_int, a_frac); } } else if (memcmp(config, "af ", 3) == 0) { - _prefs->airtime_factor = atof(&config[3]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "int.thresh ", 11) == 0) { - _prefs->interference_threshold = atoi(&config[11]); - 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, "multi.acks ", 11) == 0) { - _prefs->multi_acks = atoi(&config[11]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "allow.read.only ", 16) == 0) { - _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0; - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "flood.advert.interval ", 22) == 0) { - int hours = _atoi(&config[22]); - if ((hours > 0 && hours < 3) || (hours > 168)) { - strcpy(reply, "Error: interval range is 3-168 hours"); - } else { - _prefs->flood_advert_interval = (uint8_t)(hours); - _callbacks->updateFloodAdvertTimer(); - savePrefs(); - strcpy(reply, "OK"); - } - } else if (memcmp(config, "advert.interval ", 16) == 0) { - int mins = _atoi(&config[16]); - if ((mins > 0 && mins < MIN_LOCAL_ADVERT_INTERVAL) || (mins > 240)) { - sprintf(reply, "Error: interval range is %d-240 minutes", MIN_LOCAL_ADVERT_INTERVAL); - } else { - _prefs->advert_interval = (uint8_t)(mins / 2); - _callbacks->updateAdvertTimer(); - savePrefs(); - strcpy(reply, "OK"); - } - } else if (memcmp(config, "guest.password ", 15) == 0) { - StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password)); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "prv.key ", 8) == 0) { - uint8_t prv_key[PRV_KEY_SIZE]; - bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]); - // only allow rekey if key is valid - if (success && mesh::LocalIdentity::validatePrivateKey(prv_key)) { - mesh::LocalIdentity new_id; - new_id.readFrom(prv_key, PRV_KEY_SIZE); - _callbacks->saveIdentity(new_id); - strcpy(reply, "OK, reboot to apply! New pubkey: "); - mesh::Utils::toHex(&reply[33], new_id.pub_key, PUB_KEY_SIZE); - } else { - strcpy(reply, "Error, bad key"); - } - } else if (memcmp(config, "name ", 5) == 0) { - if (isValidName(&config[5])) { - StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name)); - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, bad chars"); - } - } else if (memcmp(config, "repeat ", 7) == 0) { - _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0; - savePrefs(); - strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); + _prefs->airtime_factor = atof(&config[3]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "int.thresh ", 11) == 0) { + _prefs->interference_threshold = atoi(&config[11]); + 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, "multi.acks ", 11) == 0) { + _prefs->multi_acks = atoi(&config[11]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "allow.read.only ", 16) == 0) { + _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "flood.advert.interval ", 22) == 0) { + int hours = _atoi(&config[22]); + if ((hours > 0 && hours < 3) || (hours > 168)) { + strcpy(reply, "Error: interval range is 3-168 hours"); + } else { + _prefs->flood_advert_interval = (uint8_t)(hours); + _callbacks->updateFloodAdvertTimer(); + savePrefs(); + strcpy(reply, "OK"); + } + } else if (memcmp(config, "advert.interval ", 16) == 0) { + int mins = _atoi(&config[16]); + if ((mins > 0 && mins < MIN_LOCAL_ADVERT_INTERVAL) || (mins > 240)) { + sprintf(reply, "Error: interval range is %d-240 minutes", MIN_LOCAL_ADVERT_INTERVAL); + } else { + _prefs->advert_interval = (uint8_t)(mins / 2); + _callbacks->updateAdvertTimer(); + savePrefs(); + strcpy(reply, "OK"); + } + } else if (memcmp(config, "guest.password ", 15) == 0) { + StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password)); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "prv.key ", 8) == 0) { + uint8_t prv_key[PRV_KEY_SIZE]; + bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]); + // only allow rekey if key is valid + if (success && mesh::LocalIdentity::validatePrivateKey(prv_key)) { + mesh::LocalIdentity new_id; + new_id.readFrom(prv_key, PRV_KEY_SIZE); + _callbacks->saveIdentity(new_id); + strcpy(reply, "OK, reboot to apply! New pubkey: "); + mesh::Utils::toHex(&reply[33], new_id.pub_key, PUB_KEY_SIZE); + } else { + strcpy(reply, "Error, bad key"); + } + } else if (memcmp(config, "name ", 5) == 0) { + if (isValidName(&config[5])) { + StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name)); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, bad chars"); + } + } else if (memcmp(config, "repeat ", 7) == 0) { + _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0; + savePrefs(); + strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); #if defined(USE_SX1262) || defined(USE_SX1268) } else if (memcmp(config, "radio.rxgain ", 13) == 0) { - _prefs->rx_boosted_gain = memcmp(&config[13], "on", 2) == 0; - strcpy(reply, "OK"); - savePrefs(); - _callbacks->setRxBoostedGain(_prefs->rx_boosted_gain); + _prefs->rx_boosted_gain = memcmp(&config[13], "on", 2) == 0; + strcpy(reply, "OK"); + savePrefs(); + _callbacks->setRxBoostedGain(_prefs->rx_boosted_gain); #endif - } else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) { - if (!_board->canControlLoRaFemLna()) { - strcpy(reply, "Error: unsupported by this board"); - } else if (memcmp(&config[17], "on", 2) == 0) { - if (_board->setLoRaFemLnaEnabled(true)) { - _prefs->radio_fem_rxgain = 1; - savePrefs(); - strcpy(reply, "OK - LoRa FEM RX gain on"); - } else { - strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); - } - } else if (memcmp(&config[17], "off", 3) == 0) { - if (_board->setLoRaFemLnaEnabled(false)) { - _prefs->radio_fem_rxgain = 0; - savePrefs(); - strcpy(reply, "OK - LoRa FEM RX gain off"); - } else { - strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); - } - } else { - strcpy(reply, "Error: state must be on or off"); - } - } 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 = num > 0 ? strtof(parts[0], nullptr) : 0.0f; - float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f; - uint8_t sf = num > 2 ? atoi(parts[2]) : 0; - uint8_t cr = num > 3 ? atoi(parts[3]) : 0; - if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) { - _prefs->sf = sf; - _prefs->cr = cr; - _prefs->freq = freq; - _prefs->bw = bw; - _callbacks->savePrefs(); - strcpy(reply, "OK - reboot to apply"); - } else { - strcpy(reply, "Error, invalid radio params"); - } - } else if (memcmp(config, "lat ", 4) == 0) { - _prefs->node_lat = atof(&config[4]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "lon ", 4) == 0) { - _prefs->node_lon = atof(&config[4]); + } else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) { + if (!_board->canControlLoRaFemLna()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&config[17], "on", 2) == 0) { + if (_board->setLoRaFemLnaEnabled(true)) { + _prefs->radio_fem_rxgain = 1; savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "rxdelay ", 8) == 0) { - float db = atof(&config[8]); - if (db >= 0) { - _prefs->rx_delay_base = db; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, cannot be negative"); - } - } else if (memcmp(config, "txdelay ", 8) == 0) { - float f = atof(&config[8]); - if (f >= 0) { - _prefs->tx_delay_factor = f; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, cannot be negative"); - } - } else if (memcmp(config, "flood.max ", 10) == 0) { - uint8_t m = atoi(&config[10]); - if (m <= 64) { - _prefs->flood_max = m; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, max 64"); - } - } else if (memcmp(config, "direct.txdelay ", 15) == 0) { - float f = atof(&config[15]); - if (f >= 0) { - _prefs->direct_tx_delay_factor = f; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, cannot be negative"); - } - } else if (memcmp(config, "owner.info ", 11) == 0) { - config += 11; - char *dp = _prefs->owner_info; - while (*config && dp - _prefs->owner_info < sizeof(_prefs->owner_info)-1) { - *dp++ = (*config == '|') ? '\n' : *config; // translate '|' to newline chars - config++; - } - *dp = 0; - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "path.hash.mode ", 15) == 0) { - config += 15; - uint8_t mode = atoi(config); - if (mode < 3) { - _prefs->path_hash_mode = mode; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be 0,1, or 2"); - } - } else if (memcmp(config, "loop.detect ", 12) == 0) { - config += 12; - uint8_t mode; - if (memcmp(config, "off", 3) == 0) { - mode = LOOP_DETECT_OFF; - } else if (memcmp(config, "minimal", 7) == 0) { - mode = LOOP_DETECT_MINIMAL; - } else if (memcmp(config, "moderate", 8) == 0) { - mode = LOOP_DETECT_MODERATE; - } else if (memcmp(config, "strict", 6) == 0) { - mode = LOOP_DETECT_STRICT; - } else { - mode = 0xFF; - strcpy(reply, "Error, must be: off, minimal, moderate, or strict"); - } - if (mode != 0xFF) { - _prefs->loop_detect = mode; - savePrefs(); - strcpy(reply, "OK"); - } - } 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) { - _prefs->freq = atof(&config[5]); + strcpy(reply, "OK - LoRa FEM RX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else if (memcmp(&config[17], "off", 3) == 0) { + if (_board->setLoRaFemLnaEnabled(false)) { + _prefs->radio_fem_rxgain = 0; savePrefs(); - strcpy(reply, "OK - reboot to apply"); + strcpy(reply, "OK - LoRa FEM RX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + } 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 = num > 0 ? strtof(parts[0], nullptr) : 0.0f; + float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f; + uint8_t sf = num > 2 ? atoi(parts[2]) : 0; + uint8_t cr = num > 3 ? atoi(parts[3]) : 0; + if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) { + _prefs->sf = sf; + _prefs->cr = cr; + _prefs->freq = freq; + _prefs->bw = bw; + _callbacks->savePrefs(); + strcpy(reply, "OK - reboot to apply"); + } else { + strcpy(reply, "Error, invalid radio params"); + } + } else if (memcmp(config, "lat ", 4) == 0) { + _prefs->node_lat = atof(&config[4]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "lon ", 4) == 0) { + _prefs->node_lon = atof(&config[4]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "rxdelay ", 8) == 0) { + float db = atof(&config[8]); + if (db >= 0) { + _prefs->rx_delay_base = db; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, cannot be negative"); + } + } else if (memcmp(config, "txdelay ", 8) == 0) { + float f = atof(&config[8]); + if (f >= 0) { + _prefs->tx_delay_factor = f; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, cannot be negative"); + } + } else if (memcmp(config, "flood.max ", 10) == 0) { + uint8_t m = atoi(&config[10]); + if (m <= 64) { + _prefs->flood_max = m; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, max 64"); + } + } else if (memcmp(config, "direct.txdelay ", 15) == 0) { + float f = atof(&config[15]); + if (f >= 0) { + _prefs->direct_tx_delay_factor = f; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, cannot be negative"); + } + } else if (memcmp(config, "owner.info ", 11) == 0) { + config += 11; + char *dp = _prefs->owner_info; + while (*config && dp - _prefs->owner_info < sizeof(_prefs->owner_info)-1) { + *dp++ = (*config == '|') ? '\n' : *config; // translate '|' to newline chars + config++; + } + *dp = 0; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "path.hash.mode ", 15) == 0) { + config += 15; + uint8_t mode = atoi(config); + if (mode < 3) { + _prefs->path_hash_mode = mode; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0,1, or 2"); + } + } else if (memcmp(config, "loop.detect ", 12) == 0) { + config += 12; + uint8_t mode; + if (memcmp(config, "off", 3) == 0) { + mode = LOOP_DETECT_OFF; + } else if (memcmp(config, "minimal", 7) == 0) { + mode = LOOP_DETECT_MINIMAL; + } else if (memcmp(config, "moderate", 8) == 0) { + mode = LOOP_DETECT_MODERATE; + } else if (memcmp(config, "strict", 6) == 0) { + mode = LOOP_DETECT_STRICT; + } else { + mode = 0xFF; + strcpy(reply, "Error, must be: off, minimal, moderate, or strict"); + } + if (mode != 0xFF) { + _prefs->loop_detect = mode; + savePrefs(); + strcpy(reply, "OK"); + } + } 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) { + _prefs->freq = atof(&config[5]); + savePrefs(); + strcpy(reply, "OK - reboot to apply"); #ifdef WITH_BRIDGE - } else if (memcmp(config, "bridge.enabled ", 15) == 0) { - _prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 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) { - _prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0; - savePrefs(); - strcpy(reply, "OK"); + } else if (memcmp(config, "bridge.enabled ", 15) == 0) { + _prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 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) { + _prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0; + savePrefs(); + strcpy(reply, "OK"); #endif #ifdef WITH_RS232_BRIDGE - } else if (memcmp(config, "bridge.baud ", 12) == 0) { - uint32_t baud = atoi(&config[12]); - if (baud >= 9600 && baud <= BRIDGE_MAX_BAUD) { - _prefs->bridge_baud = (uint32_t)baud; - _callbacks->restartBridge(); - savePrefs(); - strcpy(reply, "OK"); - } else { - sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD); - } + } else if (memcmp(config, "bridge.baud ", 12) == 0) { + uint32_t baud = atoi(&config[12]); + if (baud >= 9600 && baud <= BRIDGE_MAX_BAUD) { + _prefs->bridge_baud = (uint32_t)baud; + _callbacks->restartBridge(); + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD); + } #endif #ifdef WITH_ESPNOW_BRIDGE - } else if (memcmp(config, "bridge.channel ", 15) == 0) { - int ch = atoi(&config[15]); - if (ch > 0 && ch < 15) { - _prefs->bridge_channel = (uint8_t)ch; - _callbacks->restartBridge(); - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error: channel must be between 1-14"); - } - } else if (memcmp(config, "bridge.secret ", 14) == 0) { - StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret)); - _callbacks->restartBridge(); - savePrefs(); - strcpy(reply, "OK"); + } else if (memcmp(config, "bridge.channel ", 15) == 0) { + int ch = atoi(&config[15]); + if (ch > 0 && ch < 15) { + _prefs->bridge_channel = (uint8_t)ch; + _callbacks->restartBridge(); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error: channel must be between 1-14"); + } + } else if (memcmp(config, "bridge.secret ", 14) == 0) { + StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret)); + _callbacks->restartBridge(); + savePrefs(); + strcpy(reply, "OK"); #endif - } else if (memcmp(config, "adc.multiplier ", 15) == 0) { - _prefs->adc_multiplier = atof(&config[15]); - if (_board->setAdcMultiplier(_prefs->adc_multiplier)) { - savePrefs(); - if (_prefs->adc_multiplier == 0.0f) { - strcpy(reply, "OK - using default board multiplier"); - } else { - sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier); - } - } else { - _prefs->adc_multiplier = 0.0f; - strcpy(reply, "Error: unsupported by this board"); - }; + } else if (memcmp(config, "adc.multiplier ", 15) == 0) { + _prefs->adc_multiplier = atof(&config[15]); + if (_board->setAdcMultiplier(_prefs->adc_multiplier)) { + savePrefs(); + if (_prefs->adc_multiplier == 0.0f) { + strcpy(reply, "OK - using default board multiplier"); } else { - sprintf(reply, "unknown config: %s", config); + sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier); } + } else { + _prefs->adc_multiplier = 0.0f; + strcpy(reply, "Error: unsupported"); + }; + } else { + strcpy(reply, "unknown config: "); + } } void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) { @@ -837,6 +837,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else if (memcmp(config, "radio.rxgain", 12) == 0) { sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off"); #endif + } else if (memcmp(config, "radio.fem.rxgain", 16) == 0) { + if (!_board->canControlLoRaFemLna()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off"); + } } else if (memcmp(config, "radio", 5) == 0) { char freq[16], bw[16]; strcpy(freq, StrHelper::ftoa(_prefs->freq)); @@ -868,9 +874,9 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "> minimal"); } else if (_prefs->loop_detect == LOOP_DETECT_MODERATE) { strcpy(reply, "> moderate"); - } else { + } else { strcpy(reply, "> strict"); - } + } } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) { sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm); } else if (memcmp(config, "freq", 4) == 0) { @@ -917,12 +923,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "> unknown"); } #else - strcpy(reply, "ERROR: unsupported"); + strcpy(reply, "Error: unsupported"); #endif } else if (memcmp(config, "adc.multiplier", 14) == 0) { float adc_mult = _board->getAdcMultiplier(); if (adc_mult == 0.0f) { - strcpy(reply, "Error: unsupported by this board"); + strcpy(reply, "Error: unsupported"); } else { sprintf(reply, "> %.3f", adc_mult); } diff --git a/variants/heltec_t096/LoRaFEMControl.h b/variants/heltec_t096/LoRaFEMControl.h index 2c50b74289..a3b5c4ed9b 100644 --- a/variants/heltec_t096/LoRaFEMControl.h +++ b/variants/heltec_t096/LoRaFEMControl.h @@ -12,10 +12,11 @@ class LoRaFEMControl void setRxModeEnable(void); void setRxModeEnableWhenMCUSleep(void); void setLNAEnable(bool enabled); - bool isLnaCanControl(void) { return lna_can_control; } + bool isLnaCanControl(void) const { return lna_can_control; } void setLnaCanControl(bool can_control) { lna_can_control = can_control; } + bool isLNAEnabled(void) const { return lna_enabled; } private: - bool lna_enabled = false; + bool lna_enabled = true; bool lna_can_control = false; }; diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 550131571f..54425145c4 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -123,4 +123,22 @@ void T096Board::powerOff() { const char* T096Board::getManufacturerName() const { return "Heltec T096"; -} \ No newline at end of file +} + +bool T096Board::setLoRaFemLnaEnabled(bool enable) { + if (!loRaFEMControl.isLnaCanControl()) { + return false; + } + + loRaFEMControl.setLNAEnable(enable); + loRaFEMControl.setRxModeEnable(); + return true; +} + +bool T096Board::canControlLoRaFemLna() const { + return loRaFEMControl.isLnaCanControl(); +} + +bool T096Board::isLoRaFemLnaEnabled() const { + return loRaFEMControl.isLNAEnabled(); +} diff --git a/variants/heltec_t096/T096Board.h b/variants/heltec_t096/T096Board.h index d1e3bdfdee..15c7e68b5d 100644 --- a/variants/heltec_t096/T096Board.h +++ b/variants/heltec_t096/T096Board.h @@ -25,4 +25,7 @@ class T096Board : public NRF52BoardDCDC { uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; void powerOff() override; + bool setLoRaFemLnaEnabled(bool enable) override; + bool canControlLoRaFemLna() const override; + bool isLoRaFemLnaEnabled() const override; }; diff --git a/variants/heltec_tracker_v2/LoRaFEMControl.h b/variants/heltec_tracker_v2/LoRaFEMControl.h index 0ce60fffd8..a3b5c4ed9b 100644 --- a/variants/heltec_tracker_v2/LoRaFEMControl.h +++ b/variants/heltec_tracker_v2/LoRaFEMControl.h @@ -17,6 +17,6 @@ class LoRaFEMControl bool isLNAEnabled(void) const { return lna_enabled; } private: - bool lna_enabled = false; + bool lna_enabled = true; bool lna_can_control = false; }; diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index f9dafb3b77..87791e6b6f 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -20,7 +20,7 @@ void HeltecV4Board::begin() { rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS); rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); } -} + } void HeltecV4Board::onBeforeTransmit(void) { digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on diff --git a/variants/heltec_v4/HeltecV4Board.h b/variants/heltec_v4/HeltecV4Board.h index fe77caed05..f96e161c8d 100644 --- a/variants/heltec_v4/HeltecV4Board.h +++ b/variants/heltec_v4/HeltecV4Board.h @@ -17,10 +17,10 @@ class HeltecV4Board : public ESP32Board { void onAfterTransmit(void) override; void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); void powerOff() override; - uint16_t getBattMilliVolts() override; - const char* getManufacturerName() const override ; bool setLoRaFemLnaEnabled(bool enable) override; bool canControlLoRaFemLna() const override; bool isLoRaFemLnaEnabled() const override; + uint16_t getBattMilliVolts() override; + const char* getManufacturerName() const override ; }; From 6c20a1062fd46d1cb18b7dc0d7c930227d96f04e Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 27 Apr 2026 16:33:39 -0700 Subject: [PATCH 19/94] Refine direct retry SNR handling and recent repeater controls --- docs/cli_commands.md | 28 +- examples/simple_repeater/MyMesh.cpp | 432 ++++++++++++++++++++-------- examples/simple_repeater/MyMesh.h | 1 + src/Mesh.cpp | 15 +- src/helpers/CommonCLI.cpp | 75 ++++- src/helpers/CommonCLI.h | 2 +- src/helpers/SimpleMeshTables.h | 194 +++++++++++-- 7 files changed, 577 insertions(+), 170 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index b6c87a7f02..9b4723e329 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -118,24 +118,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore ### Get or set recent repeater fallback prefix/SNR **Usage:** - `get recent.repeater` -- `get recent.repeater all` -- `get recent.repeater all ` -- `get recent.repeater first ` -- `get recent.repeater first ` -- `get recent.repeater last ` -- `get recent.repeater last ` -- `set recent.repeater ` +- `get recent.repeater ` +- `get recent.repeater page ` +- `set recent.repeater ` **Parameters:** -- `prefix_hex`: 1-3 bytes of next-hop prefix (hex) +- `prefix_hex_6`: Exactly 3 bytes of next-hop prefix in hex (6 chars) - `snr_db`: SNR in dB (supports decimals; stored at x4 precision) -- `count`: number of entries to print -- `offset`: zero-based row offset into the selected order +- `page`: 1-based page number **Notes:** - `set` is rejected when the prefix already exists in neighbors. -- `all` prints oldest to newest; `first` prints the oldest N; `last` prints the newest N. -- Over LoRa remote CLI, replies are packet-size limited; use `offset` to page through all rows. +- Rows are shown newest-first. +- Serial CLI prints all rows (no paging). +- Over LoRa remote CLI, page size is fixed at `4` rows; choose page with `get recent.repeater `. --- @@ -536,7 +532,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `state`: `on`|`off` -**Default:** `off` +**Default:** `on` **Note:** When enabled, a repeater can use recently-heard non-duplicate repeater prefixes as a fallback for direct retry eligibility when no suitable neighbor entry is available. @@ -548,9 +544,9 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `set direct.retry.margin ` **Parameters:** -- `value`: Margin in dB above the SF-specific receive floor (minimum `0`, default `5`) +- `value`: Margin in dB above the SF-specific receive floor (minimum `0`, maximum `40`, quarter-dB precision, default `2.5`) -**Default:** `5` +**Default:** `2.5` **Note:** The retry gate uses the active SF floor of `SF5=-2.5`, `SF6=-5`, `SF7=-7.5`, `SF8=-10`, `SF9=-12.5`, `SF10=-15`, `SF11=-17.5`, `SF12=-20`, then adds this margin. @@ -564,7 +560,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `value`: Retry attempts after initial TX (`1`-`15`) -**Default:** `3` +**Default:** `15` --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 907ce2c8da..29b126fc37 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -86,6 +86,64 @@ bool MyMesh::allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefi return self->findNeighbourByHash(prefix, prefix_len) == NULL; } +static void formatRecentRepeaterPrefix(const SimpleMeshTables::RecentRepeaterInfo* info, char* out, size_t out_len) { + if (out == NULL || out_len == 0) { + return; + } + out[0] = 0; + if (info == NULL) { + return; + } + + uint8_t prefix_len = info->prefix_len; + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + if (prefix_len > 0) { + mesh::Utils::toHex(out, info->prefix, prefix_len); + } + + size_t used = strlen(out); + const size_t target_len = MAX_ROUTE_HASH_BYTES * 2; + while (used < target_len && used + 1 < out_len) { + out[used++] = ' '; + } + out[used] = 0; +} + +static void formatRecentRepeaterSnrX4(int8_t snr_x4, char* out, size_t out_len) { + if (out == NULL || out_len == 0) { + return; + } + + const char* snr_text = StrHelper::ftoa(((float)snr_x4) / 4.0f); + if (snr_text[0] == '-') { + snprintf(out, out_len, "%s", snr_text); + } else { + snprintf(out, out_len, " %s", snr_text); + } +} + +static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) { + uint8_t code = flags & 0x03; + uint8_t size_pow2 = (uint8_t)(1U << code); // legacy TRACE interpretation + uint8_t size_linear = (uint8_t)(code + 1U); // packed-size interpretation (1..4) + + bool pow2_ok = size_pow2 > 0 && (route_bytes % size_pow2) == 0; + bool linear_ok = size_linear > 0 && (route_bytes % size_linear) == 0; + + if (pow2_ok && !linear_ok) { + return size_pow2; + } + if (linear_ok && !pow2_ok) { + return size_linear; + } + if (pow2_ok) { + return size_pow2; + } + return size_linear; +} + void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { #if MAX_NEIGHBOURS // check if neighbours enabled // find existing neighbour, else use least recently updated @@ -456,6 +514,30 @@ void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, ui bool MyMesh::allowPacketForward(const mesh::Packet *packet) { if (_prefs.disable_fwd) return false; + + if (packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE && packet->payload_len >= 9) { + auto* tables = (SimpleMeshTables *)getTables(); + uint8_t route_bytes = packet->payload_len - 9; + uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes); + uint16_t offset = (uint16_t)packet->path_len * (uint16_t)hash_size; + uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); + int16_t fallback_snr_x4 = direct_retry_floor_x4[sf - 5] + 40; // fixed +10 dB above SF floor + + // A successful TRACE forward reveals the downstream next-hop hash. Seed/update the recent table immediately. + if (hash_size > 0 && offset + (2U * hash_size) <= route_bytes) { + uint8_t prefix_len = hash_size; + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + const uint8_t* next_hop_prefix = &packet->payload[9 + offset + hash_size]; + const auto* existing = tables->findRecentRepeaterByHash(next_hop_prefix, prefix_len); + // This point only proves we can forward TO next_hop; packet->_snr is upstream RX and not a + // trustworthy metric for next_hop. Seed with existing table value or fallback only. + int8_t trace_snr_x4 = (existing != NULL) ? existing->snr_x4 : (int8_t)constrain(fallback_snr_x4, -128, 127); + tables->setRecentRepeater(next_hop_prefix, prefix_len, trace_snr_x4, false, true); + } + } + if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; if (packet->isRouteFlood() && recv_pkt_region == NULL) { MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet"); @@ -564,23 +646,102 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u return; } - MESH_DEBUG_PRINTLN("%s direct retry %s (type=%d, route=%s, payload_len=%d, delay=%lu)", + uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; + uint8_t prefix_len = 0; + bool has_prefix = extractDirectRetryPrefix(packet, prefix, prefix_len); + auto* tables = (SimpleMeshTables *)getTables(); + const auto* existing = has_prefix ? tables->findRecentRepeaterByHash(prefix, prefix_len) : NULL; + char next_hop_hex[(MAX_ROUTE_HASH_BYTES * 2) + 1] = {0}; + if (has_prefix && prefix_len > 0) { + mesh::Utils::toHex(next_hop_hex, prefix, prefix_len); + } + const char* next_hop = (has_prefix && prefix_len > 0) ? next_hop_hex : "unknown"; + // Direct-retry events are TX-side and usually have no trustworthy RX SNR. + // Cap event SNR at fixed SF floor + 10 dB so trace-start retries can't inflate table SNR. + uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); + int16_t fallback_snr_x4_raw = direct_retry_floor_x4[sf - 5] + 40; + int8_t fallback_snr_x4 = (int8_t)constrain(fallback_snr_x4_raw, -128, 127); + bool is_success_event = (strcmp(event, "good") == 0 || strcmp(event, "canceled_echo") == 0); + int8_t retry_event_snr_x4; + const char* snr_src; + if (is_success_event && packet->_snr != 0) { + // On success, Mesh.cpp injects echo RX SNR for TRACE retries. + retry_event_snr_x4 = packet->_snr; + snr_src = "packet"; + } else if (existing != NULL) { + retry_event_snr_x4 = existing->snr_x4; + snr_src = "table"; + } else { + retry_event_snr_x4 = fallback_snr_x4; + snr_src = "fallback"; + } + char snr_used_text[12]; + char snr_pkt_text[12]; + char snr_table_text[12]; + snprintf(snr_used_text, sizeof(snr_used_text), "%s", StrHelper::ftoa(((float)retry_event_snr_x4) / 4.0f)); + snprintf(snr_pkt_text, sizeof(snr_pkt_text), "%s", StrHelper::ftoa(((float)packet->_snr) / 4.0f)); + if (existing != NULL) { + snprintf(snr_table_text, sizeof(snr_table_text), "%s", StrHelper::ftoa(((float)existing->snr_x4) / 4.0f)); + } else { + snprintf(snr_table_text, sizeof(snr_table_text), "na"); + } + + if (has_prefix && is_success_event) { + // Refresh SNR only on successful echo/progress events, not on queued/resent bookkeeping. + tables->setRecentRepeater(prefix, prefix_len, retry_event_snr_x4, false, true); + } + + if (strcmp(event, "resent") == 0) { + if (has_prefix) { + // Retry stats should be visible even when the prefix was never learned into recent.repeater. + tables->incrementRecentRepeaterRetryCount(prefix, prefix_len, true, retry_event_snr_x4, true); + } + } else if (strcmp(event, "failed_all_tries") == 0) { + if (has_prefix) { + // A failed_all_tries event means all retry attempts for this packet failed. + // Count failures by retry-attempts so fail% reflects failed retries, not just failed sessions. + uint8_t give_up_retries = getDirectRetryMaxAttempts(packet); + uint8_t failed_retries = give_up_retries; + if (failed_retries < 1) { + failed_retries = 1; + } + for (uint8_t i = 0; i < failed_retries; i++) { + tables->incrementRecentRepeaterFailCount(prefix, prefix_len, true, retry_event_snr_x4, true); + } + if (failed_retries >= give_up_retries && give_up_retries > 0) { + // If all configured retry attempts still fail, slightly degrade stored path quality. + tables->decrementRecentRepeaterSnrX4(prefix, prefix_len, 1); + } + } + } + + MESH_DEBUG_PRINTLN("%s direct retry %s (type=%d, route=%s, payload_len=%d, next_hop=%s, snr=%s, snr_src=%s, pkt_snr=%s, table_snr=%s, delay=%lu)", getLogDateTime(), event, (uint32_t)packet->getPayloadType(), packet->isRouteDirect() ? "D" : "F", (uint32_t)packet->payload_len, + next_hop, + snr_used_text, + snr_src, + snr_pkt_text, + snr_table_text, (unsigned long)delay_millis); if (_logging) { File f = openAppend(PACKET_LOG_FILE); if (f) { f.print(getLogDateTime()); - f.printf(": DIRECT RETRY %s (type=%d, route=%s, payload_len=%d, delay=%lu)\n", + f.printf(": DIRECT RETRY %s (type=%d, route=%s, payload_len=%d, next_hop=%s, snr=%s, snr_src=%s, pkt_snr=%s, table_snr=%s, delay=%lu)\n", event, (uint32_t)packet->getPayloadType(), packet->isRouteDirect() ? "D" : "F", (uint32_t)packet->payload_len, + next_hop, + snr_used_text, + snr_src, + snr_pkt_text, + snr_table_text, (unsigned long)delay_millis); f.close(); } @@ -603,28 +764,59 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { int8_t MyMesh::getDirectRetryMinSNRX4() const { // Use the live SF so `tempradio` changes immediately affect the retry threshold. uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); - int16_t threshold = direct_retry_floor_x4[sf - 5] + ((int16_t)_prefs.direct_retry_snr_margin_db * 4); + int16_t threshold = direct_retry_floor_x4[sf - 5] + (int16_t)_prefs.direct_retry_snr_margin_db; return (int8_t)constrain(threshold, -128, 127); } -bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { - if (_prefs.disable_fwd) { +bool MyMesh::extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { + if (packet == NULL || prefix == NULL) { return false; } - int8_t min_snr_x4 = getDirectRetryMinSNRX4(); - const NeighbourInfo* neighbour = findNeighbourByHash(next_hop_hash, next_hop_hash_len); - // Prefer the explicit neighbor table first; it is the strongest signal that this hop is still reachable. - if (neighbour != NULL && neighbour->snr >= min_snr_x4) { + // TRACE direct routes encode repeater hashes in payload; packet->path carries SNR trail bytes. + if (packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE && packet->payload_len >= 9) { + uint8_t route_bytes = packet->payload_len - 9; + uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes); + uint8_t offset = packet->path_len * hash_size; + if (hash_size > 0 && offset + hash_size <= route_bytes) { + prefix_len = hash_size; + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + memcpy(prefix, &packet->payload[9 + offset], prefix_len); + return true; + } + } + + if (packet->isRouteDirect() && packet->getPathHashCount() > 0) { + prefix_len = packet->getPathHashSize(); + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + if (prefix_len == 0) { + return false; + } + memcpy(prefix, packet->path, prefix_len); return true; } - if (!_prefs.direct_retry_recent_enabled) { + return false; +} +bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { + if (_prefs.disable_fwd) { return false; } - // If no neighbor entry exists, fall back to the recent-heard repeater cache keyed by the same path prefix. - const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len); - return recent != NULL && recent->snr_x4 >= min_snr_x4; + int8_t min_snr_x4 = getDirectRetryMinSNRX4(); + if (_prefs.direct_retry_recent_enabled) { + // Prefer the 64-entry recent-prefix cache first, then fall back to neighbours. + const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len); + if (recent != NULL && recent->snr_x4 >= min_snr_x4) { + return true; + } + } + + const NeighbourInfo* neighbour = findNeighbourByHash(next_hop_hash, next_hop_hash_len); + return neighbour != NULL && neighbour->snr >= min_snr_x4; } uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { uint32_t base_wait_millis = constrain((uint32_t)_prefs.direct_retry_base_ms, (uint32_t)10, (uint32_t)5000); @@ -973,9 +1165,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.tx_delay_factor = 0.5f; // was 0.25f _prefs.direct_tx_delay_factor = 0.3f; // was 0.2 - _prefs.direct_retry_recent_enabled = 0; - _prefs.direct_retry_snr_margin_db = 5; - _prefs.direct_retry_attempts = 3; + _prefs.direct_retry_recent_enabled = 1; + _prefs.direct_retry_snr_margin_db = 10; // 2.5 dB stored in x4 units + _prefs.direct_retry_attempts = 15; _prefs.direct_retry_base_ms = 200; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; @@ -1354,9 +1546,11 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply reply[0] = 0; } else if (strncmp(command, "get recent.repeater", 19) == 0 || strncmp(command, "set recent.repeater", 19) == 0 + || strncmp(command, "clear recent.repeater", 21) == 0 || strncmp(command, "recent.repeater", 15) == 0) { bool is_get = false; bool is_set = false; + bool is_clear = false; const char* sub = command; if (strncmp(command, "get recent.repeater", 19) == 0) { @@ -1365,38 +1559,55 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else if (strncmp(command, "set recent.repeater", 19) == 0) { is_set = true; sub = command + 19; + } else if (strncmp(command, "clear recent.repeater", 21) == 0) { + is_clear = true; + sub = command + 21; } else { sub = command + 15; // legacy command format } while (*sub == ' ') sub++; auto* tables = (SimpleMeshTables*)getTables(); + if (!is_get && !is_set && !is_clear && strncmp(sub, "clear", 5) == 0 && (sub[5] == 0 || sub[5] == ' ')) { + is_clear = true; + sub += 5; + while (*sub == ' ') sub++; + } - bool is_all = (strcmp(sub, "all") == 0 || strncmp(sub, "all ", 4) == 0); - bool is_first = (strncmp(sub, "first ", 6) == 0); - bool is_last = (strncmp(sub, "last ", 5) == 0); - - if (is_set || (!is_get && *sub != 0 && !is_all && !is_first && !is_last)) { + if (is_clear) { + if (*sub != 0) { + strcpy(reply, "Err - usage: clear recent.repeater"); + } else { + tables->clearRecentRepeaters(); + strcpy(reply, "OK"); + } + } else if (is_set) { char* params = (char*) sub; char* arg_snr = strchr(params, ' '); if (arg_snr == NULL) { - strcpy(reply, "Err - usage: set recent.repeater "); + strcpy(reply, "Err - usage: set recent.repeater "); } else { *arg_snr++ = 0; while (*arg_snr == ' ') arg_snr++; if (*arg_snr == 0) { - strcpy(reply, "Err - usage: set recent.repeater "); + strcpy(reply, "Err - usage: set recent.repeater "); } else { - int hex_len = strlen(params); - int prefix_len = hex_len / 2; uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; - if ((hex_len % 2) != 0 || prefix_len <= 0 || prefix_len > MAX_ROUTE_HASH_BYTES || !mesh::Utils::fromHex(prefix, prefix_len, params)) { - strcpy(reply, "Err - prefix must be 1-3 bytes hex"); + int hex_len = strlen(params); + if (hex_len != (MAX_ROUTE_HASH_BYTES * 2) || !mesh::Utils::fromHex(prefix, MAX_ROUTE_HASH_BYTES, params)) { + strcpy(reply, "Err - prefix must be exactly 3 bytes hex (6 chars)"); } else { - float snr_db = strtof(arg_snr, nullptr); + char* end_snr = NULL; + float snr_db = strtof(arg_snr, &end_snr); + while (end_snr != NULL && *end_snr == ' ') end_snr++; + if (end_snr == arg_snr || (end_snr != NULL && *end_snr != 0)) { + strcpy(reply, "Err - snr must be numeric"); + return; + } + int snr_x4 = (int)(snr_db * 4.0f + (snr_db >= 0.0f ? 0.5f : -0.5f)); snr_x4 = constrain(snr_x4, -128, 127); - if (tables->setRecentRepeater(prefix, (uint8_t)prefix_len, (int8_t)snr_x4)) { + if (tables->setRecentRepeater(prefix, MAX_ROUTE_HASH_BYTES, (int8_t)snr_x4, true)) { strcpy(reply, "OK"); } else { strcpy(reply, "Err - prefix is already in neighbors"); @@ -1404,120 +1615,82 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } } - } else if (*sub == 0) { - const auto* info = tables->getLatestRecentRepeater(); - if (info == NULL) { - strcpy(reply, "> none"); - } else { - char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; - mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); - } - } else if (is_all || is_first || is_last) { + } else { int total = tables->getRecentRepeaterCount(); if (total <= 0) { strcpy(reply, "> none"); } else { - bool newest_first = false; - int limit = total; - int offset = 0; - const char* mode = "all"; - - if (is_first || is_last) { - const char* nstr = sub + (is_first ? 6 : 5); - while (*nstr == ' ') nstr++; - if (*nstr == 0) { - strcpy(reply, "Err - usage: get recent.repeater first|last [offset]"); - return; - } + if (sender_timestamp == 0) { + // Serial CLI: print all entries (no paging). + Serial.printf("Recent repeater table (newest first, total=%d):\n", total); + for (int i = 0; i < total; i++) { + const auto* info = tables->getRecentRepeaterNewestByIdx(i); + if (info == NULL) { + continue; + } - char* end_ptr = NULL; - long parsed_count = strtol(nstr, &end_ptr, 10); - while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; - if (end_ptr == NULL || parsed_count <= 0) { - strcpy(reply, "Err - count must be > 0"); - return; + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; + formatRecentRepeaterPrefix(info, hex, sizeof(hex)); + char snr_text[12]; + formatRecentRepeaterSnrX4(info->snr_x4, snr_text, sizeof(snr_text)); + uint32_t fail_pct_x10 = 0; + if (info->retry_count > 0) { + fail_pct_x10 = (((uint32_t)info->fail_count * 1000UL) + (info->retry_count / 2U)) / (uint32_t)info->retry_count; + } + Serial.printf("%03d: %s,%s,fp=%lu.%01lu%%,r=%u,f=%u%s\n", + i + 1, + hex, + snr_text, + (unsigned long)(fail_pct_x10 / 10U), + (unsigned long)(fail_pct_x10 % 10U), + (uint32_t)info->retry_count, + (uint32_t)info->fail_count, + info->snr_locked ? ",l" : ""); + } + sprintf(reply, "> n=%d/%d", total, total); + } else { + // Remote CLI: page by fixed size to fit packet-limited reply payload. + long page_num = 1; + const long page_size = 4; + const char* arg = sub; + + if (strncmp(arg, "page ", 5) == 0) { + arg += 5; + while (*arg == ' ') arg++; } - if (*end_ptr != 0) { - char* end_ptr2 = NULL; - long parsed_offset = strtol(end_ptr, &end_ptr2, 10); - while (end_ptr2 != NULL && *end_ptr2 == ' ') end_ptr2++; - if (end_ptr2 == NULL || *end_ptr2 != 0 || parsed_offset < 0) { - strcpy(reply, "Err - offset must be >= 0"); + if (*arg != 0) { + char* end_ptr = NULL; + page_num = strtol(arg, &end_ptr, 10); + while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; + if (end_ptr == NULL || page_num <= 0 || (end_ptr != NULL && *end_ptr != 0)) { + strcpy(reply, "Err - usage: get recent.repeater [page]"); return; } - offset = (int)parsed_offset; - } - - limit = (int)parsed_count; - if (is_last) { - newest_first = true; - mode = "last"; - } else { - mode = "first"; - } - } else if (strncmp(sub, "all ", 4) == 0) { - const char* arg = sub + 4; - while (*arg == ' ') arg++; - if (*arg == 0) { - strcpy(reply, "Err - usage: get recent.repeater all "); - return; } - char* end_ptr = NULL; - long parsed_a = strtol(arg, &end_ptr, 10); - while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; - if (end_ptr == NULL || parsed_a <= 0) { - strcpy(reply, "Err - count must be > 0"); + int total_pages = (total + (int)page_size - 1) / (int)page_size; + if (page_num > total_pages) { + sprintf(reply, "> none (page=%ld/%d)", page_num, total_pages); return; } - char* end_ptr2 = NULL; - long parsed_b = strtol(end_ptr, &end_ptr2, 10); - while (end_ptr2 != NULL && *end_ptr2 == ' ') end_ptr2++; - if (end_ptr2 == NULL || *end_ptr2 != 0 || parsed_b < 0) { - strcpy(reply, "Err - usage: get recent.repeater all "); - return; + int offset = ((int)page_num - 1) * (int)page_size; + int limit = total - offset; + if (limit > (int)page_size) { + limit = (int)page_size; } - limit = (int)parsed_a; - offset = (int)parsed_b; - } - - if (offset >= total) { - sprintf(reply, "> none (%s off=%d/%d)", mode, offset, total); - return; - } - int available = total - offset; - if (limit > available) { - limit = available; - } - - if (sender_timestamp == 0) { - Serial.printf("Recent repeater table (%s %d/%d, off=%d):\n", mode, limit, total, offset); - for (int i = 0; i < limit; i++) { - int idx = offset + i; - const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(idx) : tables->getRecentRepeaterOldestByIdx(idx); - if (info == NULL) { - continue; - } - char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; - mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - Serial.printf("%02d: %s,%s\n", idx + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); - } - sprintf(reply, "> %s off=%d n=%d/%d", mode, offset, limit, total); - } else { - // Remote CLI replies are packet-bound, so include as many rows as fit. - int written = snprintf(reply, 160, "> %s off=%d n=%d/%d", mode, offset, limit, total); + int written = snprintf(reply, 160, "> page=%ld/%d n=%d/%d", page_num, total_pages, limit, total); bool truncated = false; if (written < 0) { reply[0] = 0; written = 0; } + for (int i = 0; i < limit; i++) { int idx = offset + i; - const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(idx) : tables->getRecentRepeaterOldestByIdx(idx); + const auto* info = tables->getRecentRepeaterNewestByIdx(idx); if (info == NULL) { continue; } @@ -1525,9 +1698,20 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply truncated = true; break; } + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; - mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - int n = snprintf(reply + written, 160 - written, "\n%02d:%s,%s", idx + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + formatRecentRepeaterPrefix(info, hex, sizeof(hex)); + char snr_text[12]; + formatRecentRepeaterSnrX4(info->snr_x4, snr_text, sizeof(snr_text)); + int n = snprintf(reply + written, + 160 - written, + "\n%03d:%s,%s,r=%u,f=%u%s", + idx + 1, + hex, + snr_text, + (uint32_t)info->retry_count, + (uint32_t)info->fail_count, + info->snr_locked ? ",l" : ""); if (n < 0 || n >= (160 - written)) { truncated = true; break; @@ -1535,12 +1719,10 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply written += n; } if (truncated && written < 156) { - snprintf(reply + written, 160 - written, "\n... use offset"); + snprintf(reply + written, 160 - written, "\n... next page"); } } } - } else { - strcpy(reply, "Err - usage: get recent.repeater [all|all |first [offset]|last [offset]]"); } } else if (memcmp(command, "discover.neighbors", 18) == 0) { const char* sub = command + 18; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 00a8a31b69..b2626e6021 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -123,6 +123,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { #endif const NeighbourInfo* findNeighbourByHash(const uint8_t* hash, uint8_t hash_len) const; + bool extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const; static bool allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefix_len, void* ctx); int8_t getDirectRetryMinSNRX4() const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 47fc6e8dfe..f07484d69d 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -3,7 +3,7 @@ namespace mesh { -static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 3; +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 15; static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; void Mesh::begin() { @@ -491,6 +491,12 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { } if (_direct_retries[i].queued) { + if (_direct_retries[i].expect_path_growth + && _direct_retries[i].packet != NULL + && _direct_retries[i].progress_marker < packet->path_len) { + // For retry-good quality, use the received echo packet SNR (return-link quality). + _direct_retries[i].packet->_snr = packet->_snr; + } for (int j = 0; j < _mgr->getOutboundTotal(); j++) { if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { Packet* pending = _mgr->removeOutboundByIdx(j); @@ -504,6 +510,12 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { onDirectRetryEvent("good", _direct_retries[i].packet, 0); clearDirectRetrySlot(i); } else { + if (_direct_retries[i].expect_path_growth + && _direct_retries[i].trigger_packet != NULL + && _direct_retries[i].progress_marker < packet->path_len) { + // For retry-good quality, use the received echo packet SNR (return-link quality). + _direct_retries[i].trigger_packet->_snr = packet->_snr; + } onDirectRetryEvent("canceled_echo", _direct_retries[i].trigger_packet, 0); onDirectRetryEvent("good", _direct_retries[i].trigger_packet, 0); clearDirectRetrySlot(i); @@ -532,6 +544,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX; } if (_direct_retries[i].retry_attempts_sent >= max_attempts) { + onDirectRetryEvent("failed_all_tries", packet, 0); onDirectRetryEvent("failure", packet, 0); clearDirectRetrySlot(i); continue; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 02d27830d8..9460a28fa3 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -11,12 +11,13 @@ // These bytes used to be reserved/unused in persisted prefs, so keep a marker before trusting them. #define DIRECT_RETRY_PREFS_MAGIC_0 0xD4 #define DIRECT_RETRY_PREFS_MAGIC_1 0x52 -#define DIRECT_RETRY_RECENT_DEFAULT 0 -#define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT 5 -#define DIRECT_RETRY_SNR_MARGIN_DB_MAX 40 +#define DIRECT_RETRY_RECENT_DEFAULT 1 +#define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT_X4 10 +#define DIRECT_RETRY_SNR_MARGIN_DB_MAX 40 +#define DIRECT_RETRY_SNR_MARGIN_X4_MAX (DIRECT_RETRY_SNR_MARGIN_DB_MAX * 4) #define DIRECT_RETRY_TIMING_MAGIC_0 0xD5 #define DIRECT_RETRY_TIMING_MAGIC_1 0x54 -#define DIRECT_RETRY_COUNT_DEFAULT 3 +#define DIRECT_RETRY_COUNT_DEFAULT 15 #define DIRECT_RETRY_COUNT_MIN 1 #define DIRECT_RETRY_COUNT_MAX 15 #define DIRECT_RETRY_BASE_MS_DEFAULT 200 @@ -33,6 +34,15 @@ static uint32_t _atoi(const char* sp) { return n; } +static uint8_t directRetryMarginDbToX4(float margin_db) { + int32_t scaled_x4 = (int32_t)((margin_db * 4.0f) + 0.5f); // nearest 0.25 dB + return (uint8_t)constrain(scaled_x4, 0, DIRECT_RETRY_SNR_MARGIN_X4_MAX); +} + +static float directRetryMarginX4ToDb(uint8_t margin_x4) { + return ((float)margin_x4) / 4.0f; +} + static bool isValidName(const char *n) { while (*n) { if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false; @@ -127,10 +137,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { if (_prefs->direct_retry_prefs_magic[0] != DIRECT_RETRY_PREFS_MAGIC_0 || _prefs->direct_retry_prefs_magic[1] != DIRECT_RETRY_PREFS_MAGIC_1) { _prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT; - _prefs->direct_retry_snr_margin_db = DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT; + _prefs->direct_retry_snr_margin_db = DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT_X4; } else { _prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1); - _prefs->direct_retry_snr_margin_db = constrain(_prefs->direct_retry_snr_margin_db, 0, DIRECT_RETRY_SNR_MARGIN_DB_MAX); + _prefs->direct_retry_snr_margin_db = constrain(_prefs->direct_retry_snr_margin_db, 0, DIRECT_RETRY_SNR_MARGIN_X4_MAX); } if (_prefs->direct_retry_timing_magic[0] != DIRECT_RETRY_TIMING_MAGIC_0 || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1) { @@ -385,7 +395,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else if (memcmp(config, "direct.retry.heard", 18) == 0) { sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); } else if (memcmp(config, "direct.retry.margin", 19) == 0) { - sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_snr_margin_db); + sprintf(reply, "> %s", StrHelper::ftoa(directRetryMarginX4ToDb(_prefs->direct_retry_snr_margin_db))); } else if (memcmp(config, "direct.retry.count", 18) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts); } else if (memcmp(config, "direct.retry.base", 17) == 0) { @@ -652,9 +662,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re strcpy(reply, "Error, must be on or off"); } } else if (memcmp(config, "direct.retry.margin ", 20) == 0) { - int db = atoi(&config[20]); + float db = atof(&config[20]); if (db >= 0 && db <= DIRECT_RETRY_SNR_MARGIN_DB_MAX) { - _prefs->direct_retry_snr_margin_db = (uint8_t)db; + _prefs->direct_retry_snr_margin_db = directRetryMarginDbToX4(db); savePrefs(); strcpy(reply, "OK"); } else { @@ -1113,6 +1123,45 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, cannot be negative"); } + } else if (memcmp(config, "direct.retry.heard ", 19) == 0) { + if (memcmp(&config[19], "on", 2) == 0) { + _prefs->direct_retry_recent_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(&config[19], "off", 3) == 0) { + _prefs->direct_retry_recent_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be on or off"); + } + } else if (memcmp(config, "direct.retry.margin ", 20) == 0) { + float db = atof(&config[20]); + if (db >= 0 && db <= DIRECT_RETRY_SNR_MARGIN_DB_MAX) { + _prefs->direct_retry_snr_margin_db = directRetryMarginDbToX4(db); + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min 0 and max %d", DIRECT_RETRY_SNR_MARGIN_DB_MAX); + } + } else if (memcmp(config, "direct.retry.count ", 19) == 0) { + int count = atoi(&config[19]); + if (count >= DIRECT_RETRY_COUNT_MIN && count <= DIRECT_RETRY_COUNT_MAX) { + _prefs->direct_retry_attempts = (uint8_t)count; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_COUNT_MIN, DIRECT_RETRY_COUNT_MAX); + } + } else if (memcmp(config, "direct.retry.base ", 18) == 0) { + int delay_ms = atoi(&config[18]); + if (delay_ms >= DIRECT_RETRY_BASE_MS_MIN && delay_ms <= DIRECT_RETRY_BASE_MS_MAX) { + _prefs->direct_retry_base_ms = (uint16_t)delay_ms; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_BASE_MS_MIN, DIRECT_RETRY_BASE_MS_MAX); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; @@ -1282,6 +1331,14 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); } else if (memcmp(config, "direct.txdelay", 14) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); + } else if (memcmp(config, "direct.retry.heard", 18) == 0) { + sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); + } else if (memcmp(config, "direct.retry.margin", 19) == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(directRetryMarginX4ToDb(_prefs->direct_retry_snr_margin_db))); + } else if (memcmp(config, "direct.retry.count", 18) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts); + } else if (memcmp(config, "direct.retry.base", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_base_ms); } else if (memcmp(config, "owner.info", 10) == 0) { *reply++ = '>'; *reply++ = ' '; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 03b1fb649b..ddc92ff1ae 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -34,7 +34,7 @@ struct NodePrefs { // persisted to file char guest_password[16]; float direct_tx_delay_factor; uint8_t direct_retry_recent_enabled; - uint8_t direct_retry_snr_margin_db; + uint8_t direct_retry_snr_margin_db; // stored in quarter-dB units (x4) uint8_t direct_retry_prefs_magic[2]; uint8_t sf; uint8_t cr; diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index effea219b5..ac6d01b57b 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -8,7 +8,14 @@ #define MAX_PACKET_HASHES 128 #define MAX_PACKET_ACKS 64 -#define MAX_RECENT_REPEATERS 64 +#ifndef MAX_RECENT_REPEATERS + // Two defaults. Can be overridden with -D MAX_RECENT_REPEATERS=. + #if defined(ESP32) + #define MAX_RECENT_REPEATERS 512 + #else + #define MAX_RECENT_REPEATERS 64 + #endif +#endif #define MAX_ROUTE_HASH_BYTES 3 class SimpleMeshTables : public mesh::MeshTables { @@ -17,9 +24,12 @@ class SimpleMeshTables : public mesh::MeshTables { struct RecentRepeaterInfo { // Just enough identity to match a next-hop path prefix plus the SNR that heard it. + uint16_t retry_count; + uint16_t fail_count; uint8_t prefix[MAX_ROUTE_HASH_BYTES]; uint8_t prefix_len; int8_t snr_x4; + uint8_t snr_locked; }; private: @@ -68,19 +78,20 @@ class SimpleMeshTables : public mesh::MeshTables { return n > 0 && memcmp(a, b, n) == 0; } - int8_t avgSnrX4RoundUp(int8_t curr_snr_x4, int8_t new_snr_x4) const { - int16_t sum = (int16_t)curr_snr_x4 + (int16_t)new_snr_x4; - int16_t avg = sum / 2; // truncates toward zero - // "Round up" means ceil(), which only differs from truncation for positive odd sums. - if (sum > 0 && (sum & 1)) { - avg++; + int8_t weightedSnrX4RoundUp(int8_t curr_snr_x4, int8_t new_snr_x4) const { + // Keep existing SNR heavier than a single new sample: 75% existing + 25% new. + int16_t weighted_sum = ((int16_t)curr_snr_x4 * 3) + (int16_t)new_snr_x4; + int16_t blended = weighted_sum / 4; // truncates toward zero + // "Round up" means ceil(), which only differs from truncation for positive remainders. + if (weighted_sum > 0 && (weighted_sum % 4) != 0) { + blended++; } - if (avg > 127) { - avg = 127; - } else if (avg < -128) { - avg = -128; + if (blended > 127) { + blended = 127; + } else if (blended < -128) { + blended = -128; } - return (int8_t)avg; + return (int8_t)blended; } bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { @@ -249,7 +260,8 @@ class SimpleMeshTables : public mesh::MeshTables { _recent_repeater_allow_fn = fn; _recent_repeater_allow_ctx = ctx; } - bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) { + bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4, bool snr_locked = false, + bool bypass_allow_filter = false) { if (prefix == NULL || prefix_len == 0) { return false; } @@ -258,7 +270,8 @@ class SimpleMeshTables : public mesh::MeshTables { prefix_len = MAX_ROUTE_HASH_BYTES; } - if (_recent_repeater_allow_fn != NULL && !_recent_repeater_allow_fn(prefix, prefix_len, _recent_repeater_allow_ctx)) { + if (!bypass_allow_filter && _recent_repeater_allow_fn != NULL + && !_recent_repeater_allow_fn(prefix, prefix_len, _recent_repeater_allow_ctx)) { return false; } @@ -273,19 +286,160 @@ class SimpleMeshTables : public mesh::MeshTables { memcpy(existing.prefix, prefix, prefix_len); existing.prefix_len = prefix_len; } - existing.snr_x4 = avgSnrX4RoundUp(existing.snr_x4, snr_x4); + if (snr_locked) { + existing.snr_x4 = snr_x4; + existing.snr_locked = 1; + } else if (!existing.snr_locked) { + existing.snr_x4 = weightedSnrX4RoundUp(existing.snr_x4, snr_x4); + } return true; } - // Ring buffer is enough here; retry fallback only needs a recent prefix->SNR observation. - RecentRepeaterInfo& slot = _recent_repeaters[_next_recent_repeater_idx]; + int slot_idx = -1; + // Prefer empty slots first while preserving newest-order iteration. + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx + i) % MAX_RECENT_REPEATERS; + if (_recent_repeaters[idx].prefix_len == 0) { + slot_idx = idx; + break; + } + } + if (slot_idx < 0) { + // Table is full: evict the weakest observed SNR entry. + slot_idx = 0; + int8_t min_snr_x4 = _recent_repeaters[0].snr_x4; + for (int i = 1; i < MAX_RECENT_REPEATERS; i++) { + if (_recent_repeaters[i].snr_x4 < min_snr_x4) { + min_snr_x4 = _recent_repeaters[i].snr_x4; + slot_idx = i; + } + } + } + + RecentRepeaterInfo& slot = _recent_repeaters[slot_idx]; memset(slot.prefix, 0, sizeof(slot.prefix)); memcpy(slot.prefix, prefix, prefix_len); slot.prefix_len = prefix_len; slot.snr_x4 = snr_x4; - _next_recent_repeater_idx = (_next_recent_repeater_idx + 1) % MAX_RECENT_REPEATERS; + slot.retry_count = 0; + slot.fail_count = 0; + slot.snr_locked = snr_locked ? 1 : 0; + _next_recent_repeater_idx = (slot_idx + 1) % MAX_RECENT_REPEATERS; return true; } + bool incrementRecentRepeaterRetryCount(const uint8_t* prefix, uint8_t prefix_len, + bool create_if_missing = false, int8_t seed_snr_x4 = 0, + bool bypass_allow_filter = false) { + if (prefix == NULL || prefix_len == 0) { + return false; + } + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (prefix_len > existing.prefix_len) { + memset(existing.prefix, 0, sizeof(existing.prefix)); + memcpy(existing.prefix, prefix, prefix_len); + existing.prefix_len = prefix_len; + } + if (existing.retry_count < 0xFFFF) { + existing.retry_count++; + } + return true; + } + + if (!create_if_missing || !setRecentRepeater(prefix, prefix_len, seed_snr_x4, false, bypass_allow_filter)) { + return false; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (existing.retry_count < 0xFFFF) { + existing.retry_count++; + } + return true; + } + return false; + } + bool incrementRecentRepeaterFailCount(const uint8_t* prefix, uint8_t prefix_len, + bool create_if_missing = false, int8_t seed_snr_x4 = 0, + bool bypass_allow_filter = false) { + if (prefix == NULL || prefix_len == 0) { + return false; + } + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (prefix_len > existing.prefix_len) { + memset(existing.prefix, 0, sizeof(existing.prefix)); + memcpy(existing.prefix, prefix, prefix_len); + existing.prefix_len = prefix_len; + } + if (existing.fail_count < 0xFFFF) { + existing.fail_count++; + } + return true; + } + + if (!create_if_missing || !setRecentRepeater(prefix, prefix_len, seed_snr_x4, false, bypass_allow_filter)) { + return false; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (existing.fail_count < 0xFFFF) { + existing.fail_count++; + } + return true; + } + return false; + } + bool decrementRecentRepeaterSnrX4(const uint8_t* prefix, uint8_t prefix_len, uint8_t amount_x4 = 1) { + if (prefix == NULL || prefix_len == 0 || amount_x4 == 0) { + return false; + } + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (prefix_len > existing.prefix_len) { + memset(existing.prefix, 0, sizeof(existing.prefix)); + memcpy(existing.prefix, prefix, prefix_len); + existing.prefix_len = prefix_len; + } + if (!existing.snr_locked) { + int16_t lowered = (int16_t)existing.snr_x4 - (int16_t)amount_x4; + if (lowered < -128) { + lowered = -128; + } + existing.snr_x4 = (int8_t)lowered; + } + return true; + } + return false; + } const RecentRepeaterInfo* getLatestRecentRepeater() const { for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; @@ -360,6 +514,10 @@ class SimpleMeshTables : public mesh::MeshTables { } return NULL; } + void clearRecentRepeaters() { + memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); + _next_recent_repeater_idx = 0; + } void resetStats() { _direct_dups = _flood_dups = 0; } }; From 7f02cbe80b51ce7d250e913da3b5be3d5dd34d55 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Mon, 4 May 2026 22:04:11 +0700 Subject: [PATCH 20/94] Delayed sleep for BLE companions if BLE read is busy. Reduced sleep period from 50 sticks to 10 sticks --- examples/companion_radio/main.cpp | 4 ++-- src/helpers/BaseSerialInterface.h | 1 + src/helpers/esp32/SerialBLEInterface.cpp | 4 ++++ src/helpers/esp32/SerialBLEInterface.h | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index b4e8d386e2..bb350fd047 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -266,8 +266,8 @@ void loop() { #if defined(NRF52_PLATFORM) board.sleep(0); // nrf ignores seconds param, sleeps whenever possible #else if defined(ESP32_PLATFORM) - if (!serial_interface.isWriteBusy()) { // BLE is not busy - vTaskDelay(pdMS_TO_TICKS(50)); // attempt to sleep + if (!serial_interface.isReadBusy() && !serial_interface.isWriteBusy()) { // BLE is not busy + vTaskDelay(pdMS_TO_TICKS(10)); // attempt to sleep } #endif } diff --git a/src/helpers/BaseSerialInterface.h b/src/helpers/BaseSerialInterface.h index e60927654b..ddde483091 100644 --- a/src/helpers/BaseSerialInterface.h +++ b/src/helpers/BaseSerialInterface.h @@ -15,6 +15,7 @@ class BaseSerialInterface { virtual bool isConnected() const = 0; + virtual bool isReadBusy() const = 0; virtual bool isWriteBusy() const = 0; virtual size_t writeFrame(const uint8_t src[], size_t len) = 0; virtual size_t checkRecvFrame(uint8_t dest[]) = 0; diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index dcfa0e1e34..50e1501e5e 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -182,6 +182,10 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { #define BLE_WRITE_MIN_INTERVAL 60 +bool SerialBLEInterface::isReadBusy() const { + return (recv_queue_len > 0); +} + bool SerialBLEInterface::isWriteBusy() const { return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write? } diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 965e90fd19..19e024b040 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -76,6 +76,7 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE bool isConnected() const override; + bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; From eac4f7201e03805917bbae4288862e145a413fb6 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Wed, 6 May 2026 12:38:22 +0700 Subject: [PATCH 21/94] Commented NRF52_POWER_MANAGEMENT and added PowerSaving BLE companions --- variants/gat562_mesh_evb_pro/platformio.ini | 2 +- variants/gat562_mesh_watch13/platformio.ini | 2 +- variants/heltec_t096/platformio.ini | 26 +++++++++++++++++- variants/heltec_tracker_v2/platformio.ini | 27 +++++++++++++++++++ variants/heltec_wireless_paper/platformio.ini | 23 ++++++++++++++++ 5 files changed, 77 insertions(+), 3 deletions(-) diff --git a/variants/gat562_mesh_evb_pro/platformio.ini b/variants/gat562_mesh_evb_pro/platformio.ini index cede9c97c0..f098237f99 100644 --- a/variants/gat562_mesh_evb_pro/platformio.ini +++ b/variants/gat562_mesh_evb_pro/platformio.ini @@ -5,7 +5,7 @@ board_check = true build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_evb_pro - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D RADIO_CLASS=CustomSX1262 diff --git a/variants/gat562_mesh_watch13/platformio.ini b/variants/gat562_mesh_watch13/platformio.ini index ef30829d5c..f34f0167fd 100644 --- a/variants/gat562_mesh_watch13/platformio.ini +++ b/variants/gat562_mesh_watch13/platformio.ini @@ -8,7 +8,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/gat562_mesh_watch13 -D RAK_4631 -D RAK_BOARD - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index 19b05f3ce4..00cc5d1069 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/heltec_t096 -I src/helpers/ui -D HELTEC_T096 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=21 -D P_LORA_NSS=5 -D P_LORA_RESET=16 @@ -126,6 +126,30 @@ lib_deps = ${Heltec_t096.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_t096_companion_radio_ble_femoff] +extends = Heltec_t096 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${Heltec_t096.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D ENV_INCLUDE_GPS=1 ; enable the GPS page in UI +; -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off +build_src_filter = ${Heltec_t096.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_t096.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Heltec_t096_companion_radio_usb] extends = Heltec_t096 board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index 956b1ec771..f7b87133b5 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -176,6 +176,33 @@ lib_deps = ${Heltec_tracker_v2.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:heltec_tracker_v2_companion_radio_ble_ps] +extends = Heltec_tracker_v2 +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${Heltec_tracker_v2.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=ST7735Display + -D BLE_PIN_CODE=123456 ; dynamic, random PIN + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 + ; -D BLE_DEBUG_LOGGING=1 + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_tracker_v2.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_tracker_v2.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:heltec_tracker_v2_companion_radio_wifi] extends = Heltec_tracker_v2 build_flags = diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index c6fe657d47..2e4c4c9dd5 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -64,6 +64,29 @@ lib_deps = densaugeo/base64 @ ~1.4.0 bakercp/CRC32 @ ^2.0.0 +[env:Heltec_Wireless_Paper_companion_radio_ble_ps] +extends = Heltec_Wireless_Paper_base +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${Heltec_Wireless_Paper_base.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=E213Display + -D BLE_PIN_CODE=123456 ; dynamic, random PIN + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${Heltec_Wireless_Paper_base.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_Wireless_Paper_base.lib_deps} + densaugeo/base64 @ ~1.4.0 + bakercp/CRC32 @ ^2.0.0 + [env:Heltec_Wireless_Paper_companion_radio_usb] extends = Heltec_Wireless_Paper_base build_flags = From 517d3ed6438b9c38375e6b6e345ca1773f47d3be Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Wed, 6 May 2026 12:40:52 +0700 Subject: [PATCH 22/94] Added isReadBusy for NRF52 BLE --- src/helpers/nrf52/SerialBLEInterface.cpp | 4 ++++ src/helpers/nrf52/SerialBLEInterface.h | 1 + 2 files changed, 5 insertions(+) diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index 75a4e3b064..a846e744ed 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -401,6 +401,10 @@ bool SerialBLEInterface::isConnected() const { return _isDeviceConnected && Bluefruit.connected() > 0; } +bool SerialBLEInterface::isReadBusy() const { + return (recv_queue_len > 0); +} + bool SerialBLEInterface::isWriteBusy() const { return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3); } diff --git a/src/helpers/nrf52/SerialBLEInterface.h b/src/helpers/nrf52/SerialBLEInterface.h index de1030548f..d3cc505516 100644 --- a/src/helpers/nrf52/SerialBLEInterface.h +++ b/src/helpers/nrf52/SerialBLEInterface.h @@ -66,6 +66,7 @@ class SerialBLEInterface : public BaseSerialInterface { void disable() override; bool isEnabled() const override { return _isEnabled; } bool isConnected() const override; + bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; From 3281ea2924943a368e38b04aede041e7e3b6ca1a Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Wed, 6 May 2026 15:07:59 +0700 Subject: [PATCH 23/94] To drift forward less than 7s/day (3.5 minutes/month) for ESP32-based repeaters with Power Saving. --- examples/simple_repeater/main.cpp | 2 +- src/helpers/CommonCLI.cpp | 2 +- src/helpers/ESP32Board.h | 68 ++----------------------------- 3 files changed, 5 insertions(+), 67 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index b3fbbe5b65..17a3c1923d 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -155,7 +155,7 @@ void loop() { board.sleep(1800); // nrf ignores seconds param, sleeps whenever possible #else if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep - board.sleep(1800); // Sleep. Wake up after 30 minutes or when receiving a LoRa packet + board.sleep(30); // Sleep. Wake up after some seconds or when receiving a LoRa packet } #endif } diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 1322a36cd1..20fd13f8da 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -246,7 +246,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else if (memcmp(command, "clock", 5) == 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()); + sprintf(reply, "%02d:%02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(), dt.year()); } else if (memcmp(command, "time ", 5) == 0) { // set time (to epoch seconds) uint32_t secs = _atoi(&command[5]); uint32_t curr = getRTCClock()->getCurrentTime(); diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index ee874ff06f..fe9865931d 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -24,7 +24,7 @@ class ESP32Board : public mesh::MainBoard { public: void begin() { // for future use, sub-classes SHOULD call this from their begin() - startup_reason = BD_STARTUP_NORMAL; + startup_reason = BD_STARTUP_NORMAL; #ifdef ESP32_CPU_FREQ setCpuFrequencyMhz(ESP32_CPU_FREQ); @@ -47,7 +47,7 @@ class ESP32Board : public mesh::MainBoard { #endif #else Wire.begin(); - #endif + #endif } // Temperature from ESP32 MCU @@ -66,50 +66,6 @@ class ESP32Board : public mesh::MainBoard { return P_LORA_DIO_1; // default for SX1262 } - // 27 mins drift in 28 days and 40 mins = 669 ppm - const int64_t MICROSECONDS_PER_SECOND_DRIFT = 669; - const int64_t DRIFT_THRESHOLD_US = 60000000LL; // 1 minute in microseconds - - void applyTimeTrim() { - static int64_t last_trim_us = 0; - static int64_t accumulated_drift_us = 0; - - struct timeval tv; - gettimeofday(&tv, NULL); - int64_t now_us = (int64_t)tv.tv_sec * 1000000LL + tv.tv_usec; - - if (last_trim_us == 0) { - last_trim_us = now_us; - return; - } - - // Calculate elapsed time - int64_t elapsed_us = now_us - last_trim_us; - - // Add this interval's drift to accumulated drift - accumulated_drift_us += (elapsed_us * MICROSECONDS_PER_SECOND_DRIFT) / 1000000LL; - - // Only trim when accumulated drift exceeds threshold - if (accumulated_drift_us >= DRIFT_THRESHOLD_US) { - // Calculate full seconds to trim - uint32_t seconds_to_trim = (uint32_t)(accumulated_drift_us / 1000000LL); - - // Set back the trimmed time to RTC - tv.tv_sec -= (time_t)seconds_to_trim; - settimeofday(&tv, NULL); - - // Keep the fractional remainder (anything less than a full second) - accumulated_drift_us %= 1000000LL; - - // Save for next trim - gettimeofday(&tv, NULL); - last_trim_us = (int64_t)tv.tv_sec * 1000000LL + tv.tv_usec; - } else { - // Skip trimming - last_trim_us = now_us; - } - } - void sleep(uint32_t secs) override { // Skip if not allow to sleep if (inhibit_sleep) { @@ -117,23 +73,8 @@ class ESP32Board : public mesh::MainBoard { return; } - // Use more accurate clock in sleep -#if SOC_RTC_SLOW_CLK_SUPPORT_RC_FAST_D256 - if (rtc_clk_slow_src_get() != SOC_RTC_SLOW_CLK_SRC_RC_FAST) { - - // Switch slow clock source to RC_FAST / 256 (~31.25 kHz) - rtc_clk_slow_src_set(SOC_RTC_SLOW_CLK_SRC_RC_FAST); - - // Calibrate slow clock - esp_clk_slow_boot_cal(1024); - } -#endif - - // Keep RTC 8M during sleep - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC8M, ESP_PD_OPTION_ON); - // Set GPIO wakeup - gpio_num_t wakeupPin = (gpio_num_t)getIRQGpio(); + gpio_num_t wakeupPin = (gpio_num_t)getIRQGpio(); // Configure timer wakeup if (secs > 0) { @@ -163,9 +104,6 @@ class ESP32Board : public mesh::MainBoard { // Enable CPU interrupt servicing portEXIT_CRITICAL(&sleepMux); - - // Apply the software trim to correct for RC drift - applyTimeTrim(); } uint8_t getStartupReason() const override { return startup_reason; } From 674beb0e473f2954067d25d3ec309d70fd314ba5 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 7 May 2026 01:01:52 -0700 Subject: [PATCH 24/94] Improve repeater direct retry handling --- docs/cli_commands.md | 61 +++++-- examples/simple_repeater/MyMesh.cpp | 255 +++++++++++++++++----------- examples/simple_repeater/MyMesh.h | 8 +- src/Mesh.cpp | 113 +++++++----- src/Mesh.h | 6 +- src/helpers/CommonCLI.cpp | 178 +++++++++++++++++-- src/helpers/CommonCLI.h | 21 +++ src/helpers/SimpleMeshTables.h | 107 ++---------- 8 files changed, 489 insertions(+), 260 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index c771d3793f..f9c0825976 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -128,8 +128,10 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `page`: 1-based page number **Notes:** -- `set` is rejected when the prefix already exists in neighbors. -- Rows are shown newest-first. +- `set` stores or updates the prefix in the recent repeater table. +- Rows are sorted by prefix width (3-byte, 2-byte, 1-byte), then SNR descending. +- A full direct retry failure lowers the stored SNR by `0.25 dB`. +- If a full failure has no row yet, it first seeds the row at the active retry cutoff + `2.5 dB`, then applies the `0.25 dB` penalty. - Serial CLI prints all rows (no paging). - Over LoRa remote CLI, page size is fixed at `4` rows; choose page with `get recent.repeater `. @@ -534,11 +536,13 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `value`: Direct transmit delay factor (0-2) -**Default:** `0.2` +**Default:** `0.3` + +**Note:** Direct retry waits include the same airtime-based randomized delay calculation as direct retransmits, so this factor also controls retry echo windows. --- -#### View or change whether direct retries can fall back to the recently-heard repeater list +#### View or change whether direct retries use the recent repeater blacklist **Usage:** - `get direct.retry.heard` - `set direct.retry.heard ` @@ -548,7 +552,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Default:** `on` -**Note:** When enabled, a repeater can use recently-heard non-duplicate repeater prefixes as a fallback for direct retry eligibility when no suitable neighbor entry is available. +**Note:** When enabled, the recent repeater table is the only direct retry eligibility gate. Prefixes missing from the table are assumed reachable; prefixes in the table below the active SNR gate are blocked. Neighbor data is not used. --- @@ -558,11 +562,30 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `set direct.retry.margin ` **Parameters:** -- `value`: Margin in dB above the SF-specific receive floor (minimum `0`, maximum `40`, quarter-dB precision, default `2.5`) +- `value`: Rooftop preset margin in dB above the SF-specific receive floor (minimum `0`, maximum `40`, quarter-dB precision, default `5.0`) + +**Default:** `5.0` + +**Note:** `get direct.retry.margin` returns the active preset's effective margin. The retry gate uses the active SF floor of `SF5=-2.5`, `SF6=-5`, `SF7=-7.5`, `SF8=-10`, `SF9=-12.5`, `SF10=-15`, `SF11=-17.5`, `SF12=-20`, then adds this margin. + +--- + +#### View or change the direct retry timing preset +**Usage:** +- `get direct.retry.preset` +- `set direct.retry.preset ` + +**Parameters:** +- `value`: `infra`|`rooftop`|`mobile` or `0`|`1`|`2` + +**Default:** `rooftop` (`1`) -**Default:** `2.5` +**Presets:** +- `infra` (`0`): `275 ms` base wait, `4` retries, `150 ms` added per retry, SNR gate is SF floor + `15 dB` +- `rooftop` (`1`): `175 ms` base wait, `15` retries, `100 ms` added per retry, SNR gate is SF floor + `5 dB` +- `mobile` (`2`): `175 ms` base wait, `15` retries, `50 ms` added per retry, SNR gate is the SF floor -**Note:** The retry gate uses the active SF floor of `SF5=-2.5`, `SF6=-5`, `SF7=-7.5`, `SF8=-10`, `SF9=-12.5`, `SF10=-15`, `SF11=-17.5`, `SF12=-20`, then adds this margin. +**Note:** Selecting a preset copies those values into the retry settings. You can refine `direct.retry.margin`, `direct.retry.count`, `direct.retry.base`, or `direct.retry.step` afterward. Retry delay is `direct.txdelay` jitter + base wait + packet-length airtime wait + per-attempt step. --- @@ -572,10 +595,12 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `set direct.retry.count ` **Parameters:** -- `value`: Retry attempts after initial TX (`1`-`15`) +- `value`: Maximum retry attempts after initial TX (`1`-`15`) **Default:** `15` +**Note:** The effective value is capped by total direct path length: paths of `3` hops or less use at most `8` retries, `4` hops use at most `12`, and `5+` hops use at most `15`. A queued resend is canceled early when the next-hop echo is heard. + --- #### View or change the base direct retry wait (milliseconds) @@ -586,9 +611,23 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `value`: Base wait in milliseconds (`10`-`5000`) -**Default:** `200` +**Default:** `175` + +**Note:** The configured base is added to packet-length airtime and `direct.txdelay` jitter. Preset defaults are already reduced to account for the added `direct.txdelay` component. + +--- + +#### View or change the direct retry per-attempt add time (milliseconds) +**Usage:** +- `get direct.retry.step` +- `set direct.retry.step ` + +**Parameters:** +- `value`: Milliseconds added per retry attempt (`0`-`5000`) + +**Default:** `100` -**Note:** The actual first retry wait is `base + computed_echo_wait_from_live_phy`. +**Note:** This controls the linear add after the first retry wait. For example, `base=300` and `step=150` adds `0`, `150`, `300`, ... ms across retry attempts. --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 3e3a16ae76..6b3e8d6a7d 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -63,29 +63,6 @@ #define LAZY_CONTACTS_WRITE_DELAY 5000 -const NeighbourInfo* MyMesh::findNeighbourByHash(const uint8_t* hash, uint8_t hash_len) const { -#if MAX_NEIGHBOURS - for (int i = 0; i < MAX_NEIGHBOURS; i++) { - if (neighbours[i].heard_timestamp > 0 && neighbours[i].id.isHashMatch(hash, hash_len)) { - return &neighbours[i]; - } - } -#else - (void)hash; - (void)hash_len; -#endif - return NULL; -} - -bool MyMesh::allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefix_len, void* ctx) { - if (ctx == NULL || prefix == NULL || prefix_len == 0) { - return true; - } - - const MyMesh* self = (const MyMesh*) ctx; - return self->findNeighbourByHash(prefix, prefix_len) == NULL; -} - static void formatRecentRepeaterPrefix(const SimpleMeshTables::RecentRepeaterInfo* info, char* out, size_t out_len) { if (out == NULL || out_len == 0) { return; @@ -124,6 +101,45 @@ static void formatRecentRepeaterSnrX4(int8_t snr_x4, char* out, size_t out_len) } } +static int buildSortedRecentRepeaterView(SimpleMeshTables* tables, + const SimpleMeshTables::RecentRepeaterInfo** out, + int out_cap) { + if (tables == NULL || out == NULL || out_cap <= 0) { + return 0; + } + + int total = tables->getRecentRepeaterCount(); + if (total > out_cap) { + total = out_cap; + } + + int count = 0; + for (int i = 0; i < total; i++) { + const auto* info = tables->getRecentRepeaterNewestByIdx(i); + if (info != NULL) { + out[count++] = info; + } + } + + std::stable_sort(out, out + count, [](const SimpleMeshTables::RecentRepeaterInfo* a, + const SimpleMeshTables::RecentRepeaterInfo* b) { + uint8_t a_len = a->prefix_len; + uint8_t b_len = b->prefix_len; + if (a_len > MAX_ROUTE_HASH_BYTES) a_len = MAX_ROUTE_HASH_BYTES; + if (b_len > MAX_ROUTE_HASH_BYTES) b_len = MAX_ROUTE_HASH_BYTES; + + if (a_len != b_len) { + return a_len > b_len; // 3-byte first, then 2-byte, then 1-byte + } + if (a->snr_x4 != b->snr_x4) { + return a->snr_x4 > b->snr_x4; // highest SNR first within each prefix size + } + return false; // keep original newest-first order for ties + }); + + return count; +} + static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) { uint8_t code = flags & 0x03; uint8_t size_pow2 = (uint8_t)(1U << code); // legacy TRACE interpretation @@ -641,7 +657,7 @@ void MyMesh::logTxFail(mesh::Packet *pkt, int len) { } } -void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis) { +void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { if (packet == NULL) { return; } @@ -656,92 +672,103 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u mesh::Utils::toHex(next_hop_hex, prefix, prefix_len); } const char* next_hop = (has_prefix && prefix_len > 0) ? next_hop_hex : "unknown"; + char hop_text[24]; + if (packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE && packet->payload_len >= 9) { + uint8_t route_bytes = packet->payload_len - 9; + uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes); + uint8_t total_hops = (hash_size > 0) ? (route_bytes / hash_size) : 0; + if (total_hops > 0) { + snprintf(hop_text, sizeof(hop_text), "%u/%u", (unsigned int)packet->path_len, (unsigned int)total_hops); + } else { + snprintf(hop_text, sizeof(hop_text), "unknown"); + } + } else if (packet->isRouteDirect()) { + snprintf(hop_text, sizeof(hop_text), "remaining:%u", (unsigned int)packet->getPathHashCount()); + } else { + snprintf(hop_text, sizeof(hop_text), "unknown"); + } + // Direct-retry events are TX-side and usually have no trustworthy RX SNR. // Cap event SNR at fixed SF floor + 10 dB so trace-start retries can't inflate table SNR. uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); int16_t fallback_snr_x4_raw = direct_retry_floor_x4[sf - 5] + 40; int8_t fallback_snr_x4 = (int8_t)constrain(fallback_snr_x4_raw, -128, 127); - bool is_success_event = (strcmp(event, "good") == 0 || strcmp(event, "canceled_echo") == 0); + bool is_success_event = strcmp(event, "good") == 0; + bool updates_quality = strcmp(event, "good") == 0; int8_t retry_event_snr_x4; - const char* snr_src; - if (is_success_event && packet->_snr != 0) { - // On success, Mesh.cpp injects echo RX SNR for TRACE retries. + if (is_success_event) { + // On success, Mesh.cpp injects echo RX SNR for the downstream retry target. retry_event_snr_x4 = packet->_snr; - snr_src = "packet"; } else if (existing != NULL) { retry_event_snr_x4 = existing->snr_x4; - snr_src = "table"; } else { retry_event_snr_x4 = fallback_snr_x4; - snr_src = "fallback"; } - char snr_used_text[12]; char snr_pkt_text[12]; char snr_table_text[12]; - snprintf(snr_used_text, sizeof(snr_used_text), "%s", StrHelper::ftoa(((float)retry_event_snr_x4) / 4.0f)); - snprintf(snr_pkt_text, sizeof(snr_pkt_text), "%s", StrHelper::ftoa(((float)packet->_snr) / 4.0f)); - if (existing != NULL) { - snprintf(snr_table_text, sizeof(snr_table_text), "%s", StrHelper::ftoa(((float)existing->snr_x4) / 4.0f)); - } else { - snprintf(snr_table_text, sizeof(snr_table_text), "na"); - } - if (has_prefix && is_success_event) { - // Refresh SNR only on successful echo/progress events, not on queued/resent bookkeeping. + if (has_prefix && updates_quality) { + // Refresh SNR only once per successful echo/progress event, not on queued/resent bookkeeping. tables->setRecentRepeater(prefix, prefix_len, retry_event_snr_x4, false, true); } - if (strcmp(event, "resent") == 0) { - if (has_prefix) { - // Retry stats should be visible even when the prefix was never learned into recent.repeater. - tables->incrementRecentRepeaterRetryCount(prefix, prefix_len, true, retry_event_snr_x4, true); - } - } else if (strcmp(event, "failed_all_tries") == 0) { + if (strcmp(event, "failed_all_tries") == 0) { if (has_prefix) { - // A failed_all_tries event means all retry attempts for this packet failed. - // Count failures by retry-attempts so fail% reflects failed retries, not just failed sessions. - uint8_t give_up_retries = getDirectRetryMaxAttempts(packet); - uint8_t failed_retries = give_up_retries; - if (failed_retries < 1) { - failed_retries = 1; - } - for (uint8_t i = 0; i < failed_retries; i++) { - tables->incrementRecentRepeaterFailCount(prefix, prefix_len, true, retry_event_snr_x4, true); - } - if (failed_retries >= give_up_retries && give_up_retries > 0) { - // If all configured retry attempts still fail, slightly degrade stored path quality. - tables->decrementRecentRepeaterSnrX4(prefix, prefix_len, 1); + if (existing == NULL) { + int16_t seed_snr_x4 = (int16_t)getDirectRetryMinSNRX4() + 10; // +2.5 dB over the active retry cutoff. + tables->setRecentRepeater(prefix, prefix_len, (int8_t)constrain(seed_snr_x4, -128, 127), false, true); } + // SNR is stored in quarter-dB units, so 1 lowers quality by 0.25 dB. + tables->decrementRecentRepeaterSnrX4(prefix, prefix_len, 1); } } - MESH_DEBUG_PRINTLN("%s direct retry %s (type=%d, route=%s, payload_len=%d, next_hop=%s, snr=%s, snr_src=%s, pkt_snr=%s, table_snr=%s, delay=%lu)", + snprintf(snr_pkt_text, sizeof(snr_pkt_text), "%s", StrHelper::ftoa(((float)packet->_snr) / 4.0f)); + const auto* log_existing = has_prefix ? tables->findRecentRepeaterByHash(prefix, prefix_len) : NULL; + if (log_existing != NULL) { + snprintf(snr_table_text, sizeof(snr_table_text), "%s", StrHelper::ftoa(((float)log_existing->snr_x4) / 4.0f)); + } else { + snprintf(snr_table_text, sizeof(snr_table_text), "na"); + } + + const char* time_label = "time_ms"; + if (strcmp(event, "queued") == 0 || strcmp(event, "dropped_queue_full") == 0) { + time_label = "wait_ms"; + } else if (strcmp(event, "resent") == 0 || strcmp(event, "failed_all_tries") == 0 || strcmp(event, "failure") == 0) { + time_label = "elapsed_ms"; + } else if (strcmp(event, "good") == 0) { + time_label = "echo_ms"; + } + + MESH_DEBUG_PRINTLN("%s direct retry %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%s, next_hop=%s, pkt=%s, tbl=%s, %s=%lu)", getLogDateTime(), event, + (unsigned int)retry_attempt, (uint32_t)packet->getPayloadType(), packet->isRouteDirect() ? "D" : "F", (uint32_t)packet->payload_len, + hop_text, next_hop, - snr_used_text, - snr_src, snr_pkt_text, snr_table_text, + time_label, (unsigned long)delay_millis); if (_logging) { File f = openAppend(PACKET_LOG_FILE); if (f) { f.print(getLogDateTime()); - f.printf(": DIRECT RETRY %s (type=%d, route=%s, payload_len=%d, next_hop=%s, snr=%s, snr_src=%s, pkt_snr=%s, table_snr=%s, delay=%lu)\n", + f.printf(": DIRECT RETRY %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%s, next_hop=%s, pkt=%s, tbl=%s, %s=%lu)\n", event, + (unsigned int)retry_attempt, (uint32_t)packet->getPayloadType(), packet->isRouteDirect() ? "D" : "F", (uint32_t)packet->payload_len, + hop_text, next_hop, - snr_used_text, - snr_src, snr_pkt_text, snr_table_text, + time_label, (unsigned long)delay_millis); f.close(); } @@ -764,7 +791,8 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { int8_t MyMesh::getDirectRetryMinSNRX4() const { // Use the live SF so `tempradio` changes immediately affect the retry threshold. uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); - int16_t threshold = direct_retry_floor_x4[sf - 5] + (int16_t)_prefs.direct_retry_snr_margin_db; + int16_t margin_x4 = (int16_t)_prefs.direct_retry_snr_margin_db; + int16_t threshold = direct_retry_floor_x4[sf - 5] + margin_x4; return (int8_t)constrain(threshold, -128, 127); } bool MyMesh::extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { @@ -808,18 +836,30 @@ bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_ho int8_t min_snr_x4 = getDirectRetryMinSNRX4(); if (_prefs.direct_retry_recent_enabled) { - // Prefer the 64-entry recent-prefix cache first, then fall back to neighbours. const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len); - if (recent != NULL && recent->snr_x4 >= min_snr_x4) { - return true; - } + return recent == NULL || recent->snr_x4 >= min_snr_x4; } - const NeighbourInfo* neighbour = findNeighbourByHash(next_hop_hash, next_hop_hash_len); - return neighbour != NULL && neighbour->snr >= min_snr_x4; + return true; +} +uint8_t MyMesh::getDirectRetryPreset() const { + if (_prefs.direct_retry_preset <= DIRECT_RETRY_PRESET_MOBILE) { + return _prefs.direct_retry_preset; + } + return DIRECT_RETRY_PRESET_ROOFTOP; +} +uint8_t MyMesh::getDirectRetryConfiguredMaxAttempts() const { + return constrain(_prefs.direct_retry_attempts, (uint8_t)1, (uint8_t)15); +} +uint32_t MyMesh::getDirectRetryAttemptStepMillis() const { + return constrain((uint32_t)_prefs.direct_retry_step_ms, (uint32_t)0, (uint32_t)5000); } uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { uint32_t base_wait_millis = constrain((uint32_t)_prefs.direct_retry_base_ms, (uint32_t)10, (uint32_t)5000); + if (packet == NULL) { + return base_wait_millis; + } + // Approximate LoRa line rate in kilobits/sec from the live radio params the repeater is using now. float kbps = (((float) active_sf) * active_bw * ((float) active_cr)) / ((float) (1UL << active_sf)); if (kbps <= 0.0f) { @@ -832,8 +872,36 @@ uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { return base_wait_millis + scaled_wait_millis; } uint8_t MyMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const { - (void)packet; - return constrain(_prefs.direct_retry_attempts, (uint8_t)1, (uint8_t)15); + uint8_t configured_attempts = getDirectRetryConfiguredMaxAttempts(); + uint8_t total_hops = 0; + + if (packet != NULL) { + if (packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE && packet->payload_len >= 9) { + uint8_t route_bytes = packet->payload_len - 9; + uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes); + if (hash_size > 0) { + total_hops = (uint8_t)(route_bytes / hash_size); + } + } else { + total_hops = packet->getPathHashCount(); + } + } + + uint8_t path_cap = 15; + if (total_hops <= 3) { + path_cap = 8; + } else if (total_hops == 4) { + path_cap = 12; + } + + return configured_attempts < path_cap ? configured_attempts : path_cap; +} +uint32_t MyMesh::getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) { + uint32_t retry_delay = getDirectRetryEchoDelay(packet) + ((uint32_t)attempt_idx * getDirectRetryAttemptStepMillis()); + if (packet == NULL) { + return retry_delay; + } + return getDirectRetransmitDelay(packet) + retry_delay; } bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { @@ -1166,9 +1234,11 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.tx_delay_factor = 0.5f; // was 0.25f _prefs.direct_tx_delay_factor = 0.3f; // was 0.2 _prefs.direct_retry_recent_enabled = 1; - _prefs.direct_retry_snr_margin_db = 10; // 2.5 dB stored in x4 units - _prefs.direct_retry_attempts = 15; - _prefs.direct_retry_base_ms = 200; + _prefs.direct_retry_snr_margin_db = DIRECT_RETRY_ROOFTOP_MARGIN_X4; + _prefs.direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT; + _prefs.direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS; + _prefs.direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS; + _prefs.direct_retry_preset = DIRECT_RETRY_PRESET_ROOFTOP; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; _prefs.node_lon = ADVERT_LON; @@ -1213,8 +1283,6 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc active_bw = _prefs.bw; active_sf = _prefs.sf; active_cr = _prefs.cr; - - ((SimpleMeshTables *)getTables())->setRecentRepeaterAllowFilter(&MyMesh::allowRecentRepeaterPrefixStore, this); } void MyMesh::begin(FILESYSTEM *fs) { @@ -1612,21 +1680,22 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply if (tables->setRecentRepeater(prefix, MAX_ROUTE_HASH_BYTES, (int8_t)snr_x4, true)) { strcpy(reply, "OK"); } else { - strcpy(reply, "Err - prefix is already in neighbors"); + strcpy(reply, "Err - unable to store prefix"); } } } } } else { - int total = tables->getRecentRepeaterCount(); + const SimpleMeshTables::RecentRepeaterInfo* sorted_recent[MAX_RECENT_REPEATERS]; + int total = buildSortedRecentRepeaterView(tables, sorted_recent, MAX_RECENT_REPEATERS); if (total <= 0) { strcpy(reply, "> none"); } else { if (sender_timestamp == 0) { // Serial CLI: print all entries (no paging). - Serial.printf("Recent repeater table (newest first, total=%d):\n", total); + Serial.printf("Recent repeater table (3-byte,2-byte,1-byte; SNR desc, total=%d):\n", total); for (int i = 0; i < total; i++) { - const auto* info = tables->getRecentRepeaterNewestByIdx(i); + const auto* info = sorted_recent[i]; if (info == NULL) { continue; } @@ -1635,18 +1704,9 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply formatRecentRepeaterPrefix(info, hex, sizeof(hex)); char snr_text[12]; formatRecentRepeaterSnrX4(info->snr_x4, snr_text, sizeof(snr_text)); - uint32_t fail_pct_x10 = 0; - if (info->retry_count > 0) { - fail_pct_x10 = (((uint32_t)info->fail_count * 1000UL) + (info->retry_count / 2U)) / (uint32_t)info->retry_count; - } - Serial.printf("%03d: %s,%s,fp=%lu.%01lu%%,r=%u,f=%u%s\n", - i + 1, + Serial.printf("%s,%s%s\n", hex, snr_text, - (unsigned long)(fail_pct_x10 / 10U), - (unsigned long)(fail_pct_x10 % 10U), - (uint32_t)info->retry_count, - (uint32_t)info->fail_count, info->snr_locked ? ",l" : ""); } sprintf(reply, "> n=%d/%d", total, total); @@ -1692,7 +1752,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply for (int i = 0; i < limit; i++) { int idx = offset + i; - const auto* info = tables->getRecentRepeaterNewestByIdx(idx); + const auto* info = sorted_recent[idx]; if (info == NULL) { continue; } @@ -1707,12 +1767,9 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply formatRecentRepeaterSnrX4(info->snr_x4, snr_text, sizeof(snr_text)); int n = snprintf(reply + written, 160 - written, - "\n%03d:%s,%s,r=%u,f=%u%s", - idx + 1, + "\n%s,%s%s", hex, snr_text, - (uint32_t)info->retry_count, - (uint32_t)info->fail_count, info->snr_locked ? ",l" : ""); if (n < 0 || n >= (160 - written)) { truncated = true; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index b2626e6021..1a2f505c5e 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -122,10 +122,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { ESPNowBridge bridge; #endif - const NeighbourInfo* findNeighbourByHash(const uint8_t* hash, uint8_t hash_len) const; bool extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const; - static bool allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefix_len, void* ctx); int8_t getDirectRetryMinSNRX4() const; + uint8_t getDirectRetryPreset() const; + uint8_t getDirectRetryConfiguredMaxAttempts() const; + uint32_t getDirectRetryAttemptStepMillis() const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); @@ -156,7 +157,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override; uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override; uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override; - void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis) override; + uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override; + void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override; int getInterferenceThreshold() const override { return _prefs.interference_threshold; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index f07484d69d..6c9c8080ee 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -6,10 +6,32 @@ namespace mesh { static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 15; static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; +static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) { + uint8_t code = flags & 0x03; + uint8_t size_pow2 = (uint8_t)(1U << code); // legacy TRACE interpretation + uint8_t size_linear = (uint8_t)(code + 1U); // packed-size interpretation (1..4) + + bool pow2_ok = size_pow2 > 0 && (route_bytes % size_pow2) == 0; + bool linear_ok = size_linear > 0 && (route_bytes % size_linear) == 0; + + if (pow2_ok && !linear_ok) { + return size_pow2; + } + if (linear_ok && !pow2_ok) { + return size_linear; + } + if (pow2_ok) { + return size_pow2; + } + return size_linear; +} + void Mesh::begin() { for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { _direct_retries[i].packet = NULL; _direct_retries[i].trigger_packet = NULL; + _direct_retries[i].retry_started_at = 0; + _direct_retries[i].echo_wait_started_at = 0; _direct_retries[i].retry_at = 0; _direct_retries[i].retry_delay = 0; _direct_retries[i].retry_attempts_sent = 0; @@ -60,7 +82,7 @@ uint32_t Mesh::getDirectRetryEchoDelay(const Packet* packet) const { uint8_t Mesh::getDirectRetryMaxAttempts(const Packet* packet) const { return DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT; } -uint32_t Mesh::getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) const { +uint32_t Mesh::getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) { uint32_t base = getDirectRetryEchoDelay(packet); // Keep the historical linear spacing while allowing the base wait to vary by platform/profile. return base + ((uint32_t)attempt_idx * 100UL); @@ -102,13 +124,14 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { uint32_t auth_code; memcpy(&auth_code, &pkt->payload[i], 4); i += 4; uint8_t flags = pkt->payload[i++]; - uint8_t path_sz = flags & 0x03; // NEW v1.11+: lower 2 bits is path hash size - uint8_t len = pkt->payload_len - i; - uint8_t offset = pkt->path_len << path_sz; + uint8_t hash_size = decodeTraceHashSize(flags, len); + uint16_t offset = (uint16_t)pkt->path_len * (uint16_t)hash_size; if (offset >= len) { // TRACE has reached end of given path onTraceRecv(pkt, trace_tag, auth_code, flags, pkt->path, &pkt->payload[i], len); - } else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) { + } else if (hash_size > 0 && offset + hash_size <= len + && self_id.isHashMatch(&pkt->payload[i + offset], hash_size) + && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) { // append SNR (Not hash!) pkt->path[pkt->path_len++] = (int8_t) (pkt->getSNR()*4); @@ -449,6 +472,8 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { void Mesh::clearDirectRetrySlot(int idx) { _direct_retries[idx].packet = NULL; _direct_retries[idx].trigger_packet = NULL; + _direct_retries[idx].retry_started_at = 0; + _direct_retries[idx].echo_wait_started_at = 0; _direct_retries[idx].retry_at = 0; _direct_retries[idx].retry_delay = 0; _direct_retries[idx].retry_attempts_sent = 0; @@ -490,13 +515,16 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { continue; } + int8_t echo_snr_x4 = packet->_snr; if (_direct_retries[i].queued) { - if (_direct_retries[i].expect_path_growth - && _direct_retries[i].packet != NULL - && _direct_retries[i].progress_marker < packet->path_len) { - // For retry-good quality, use the received echo packet SNR (return-link quality). - _direct_retries[i].packet->_snr = packet->_snr; + if (_direct_retries[i].packet != NULL) { + // Success quality comes from the received downstream echo, not the original upstream RX. + _direct_retries[i].packet->_snr = echo_snr_x4; } + uint32_t echo_millis = _direct_retries[i].echo_wait_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _direct_retries[i].echo_wait_started_at); + onDirectRetryEvent("good", _direct_retries[i].packet, echo_millis, _direct_retries[i].retry_attempts_sent + 1); for (int j = 0; j < _mgr->getOutboundTotal(); j++) { if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { Packet* pending = _mgr->removeOutboundByIdx(j); @@ -506,18 +534,15 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { break; } } - onDirectRetryEvent("canceled_echo", _direct_retries[i].packet, 0); - onDirectRetryEvent("good", _direct_retries[i].packet, 0); clearDirectRetrySlot(i); } else { - if (_direct_retries[i].expect_path_growth - && _direct_retries[i].trigger_packet != NULL - && _direct_retries[i].progress_marker < packet->path_len) { - // For retry-good quality, use the received echo packet SNR (return-link quality). - _direct_retries[i].trigger_packet->_snr = packet->_snr; + if (_direct_retries[i].trigger_packet != NULL) { + _direct_retries[i].trigger_packet->_snr = echo_snr_x4; } - onDirectRetryEvent("canceled_echo", _direct_retries[i].trigger_packet, 0); - onDirectRetryEvent("good", _direct_retries[i].trigger_packet, 0); + uint32_t echo_millis = _direct_retries[i].echo_wait_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _direct_retries[i].echo_wait_started_at); + onDirectRetryEvent("good", _direct_retries[i].trigger_packet, echo_millis, _direct_retries[i].retry_attempts_sent + 1); clearDirectRetrySlot(i); } cleared = true; @@ -535,7 +560,11 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { if (_direct_retries[i].queued) { if (_direct_retries[i].packet == packet) { // The retry packet itself just finished transmitting; Dispatcher will release it after this hook. - onDirectRetryEvent("resent", packet, 0); + uint32_t elapsed_millis = _direct_retries[i].retry_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at); + onDirectRetryEvent("resent", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1); + _direct_retries[i].echo_wait_started_at = _ms->getMillis(); _direct_retries[i].retry_attempts_sent++; uint8_t max_attempts = getDirectRetryMaxAttempts(packet); if (max_attempts < 1) { @@ -544,16 +573,16 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX; } if (_direct_retries[i].retry_attempts_sent >= max_attempts) { - onDirectRetryEvent("failed_all_tries", packet, 0); - onDirectRetryEvent("failure", packet, 0); + onDirectRetryEvent("failed_all_tries", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); clearDirectRetrySlot(i); continue; } Packet* retry = obtainNewPacket(); if (retry == NULL) { - onDirectRetryEvent("dropped_no_packet", packet, 0); - onDirectRetryEvent("failure", packet, 0); + onDirectRetryEvent("dropped_no_packet", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1); + onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1); clearDirectRetrySlot(i); continue; } @@ -565,10 +594,10 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { _direct_retries[i].packet = retry; _direct_retries[i].retry_delay = retry_delay; _direct_retries[i].retry_at = futureMillis(retry_delay); - onDirectRetryEvent("queued", retry, retry_delay); + onDirectRetryEvent("queued", retry, retry_delay, _direct_retries[i].retry_attempts_sent + 1); } else { - onDirectRetryEvent("dropped_queue_full", retry, retry_delay); - onDirectRetryEvent("failure", retry, 0); + onDirectRetryEvent("dropped_queue_full", retry, retry_delay, _direct_retries[i].retry_attempts_sent + 1); + onDirectRetryEvent("failure", retry, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1); clearDirectRetrySlot(i); } } @@ -582,8 +611,8 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { // Allocate the retry packet only after TX-complete so busy repeaters do not reserve pool slots early. Packet* retry = obtainNewPacket(); if (retry == NULL) { - onDirectRetryEvent("dropped_no_packet", packet, _direct_retries[i].retry_delay); - onDirectRetryEvent("failure", packet, 0); + onDirectRetryEvent("dropped_no_packet", packet, _direct_retries[i].retry_delay, 1); + onDirectRetryEvent("failure", packet, 0, 1); clearDirectRetrySlot(i); continue; } @@ -593,14 +622,17 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { // Start the echo wait only after the initial direct transmission actually completed. sendPacket(retry, _direct_retries[i].priority, _direct_retries[i].retry_delay); if (isDirectRetryQueued(retry)) { + unsigned long now = _ms->getMillis(); _direct_retries[i].packet = retry; _direct_retries[i].trigger_packet = NULL; _direct_retries[i].queued = true; _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); - onDirectRetryEvent("queued", retry, _direct_retries[i].retry_delay); + _direct_retries[i].retry_started_at = now; + _direct_retries[i].echo_wait_started_at = now; + onDirectRetryEvent("queued", retry, _direct_retries[i].retry_delay, 1); } else { - onDirectRetryEvent("dropped_queue_full", retry, _direct_retries[i].retry_delay); - onDirectRetryEvent("failure", retry, 0); + onDirectRetryEvent("dropped_queue_full", retry, _direct_retries[i].retry_delay, 1); + onDirectRetryEvent("failure", retry, 0, 1); clearDirectRetrySlot(i); } } @@ -615,16 +647,16 @@ void Mesh::clearPendingDirectRetryOnSendFail(const Packet* packet) { if (_direct_retries[i].queued) { if (_direct_retries[i].packet == packet) { // The queued retry itself failed; Dispatcher will release it after this hook. - onDirectRetryEvent("dropped_send_fail", packet, 0); - onDirectRetryEvent("failure", packet, 0); + onDirectRetryEvent("dropped_send_fail", packet, 0, _direct_retries[i].retry_attempts_sent + 1); + onDirectRetryEvent("failure", packet, 0, _direct_retries[i].retry_attempts_sent + 1); clearDirectRetrySlot(i); } continue; } if (_direct_retries[i].trigger_packet == packet) { - onDirectRetryEvent("dropped_send_fail", packet, 0); - onDirectRetryEvent("failure", packet, 0); + onDirectRetryEvent("dropped_send_fail", packet, 0, 1); + onDirectRetryEvent("failure", packet, 0, 1); clearDirectRetrySlot(i); } } @@ -665,9 +697,9 @@ bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_h return false; } - uint8_t hash_size = 1 << (packet->payload[8] & 0x03); uint8_t route_bytes = packet->payload_len - 9; - uint8_t offset = packet->path_len * hash_size; + uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes); + uint16_t offset = (uint16_t)packet->path_len * (uint16_t)hash_size; if (offset + hash_size > route_bytes) { return false; } @@ -705,6 +737,8 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { } } if (slot_idx < 0) { + onDirectRetryEvent("dropped_no_slot", packet, 0, 0); + onDirectRetryEvent("failure", packet, 0, 0); return; } @@ -713,6 +747,8 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key); _direct_retries[slot_idx].packet = NULL; _direct_retries[slot_idx].trigger_packet = const_cast(packet); + _direct_retries[slot_idx].retry_started_at = 0; + _direct_retries[slot_idx].echo_wait_started_at = 0; _direct_retries[slot_idx].retry_at = 0; _direct_retries[slot_idx].retry_delay = retry_delay; _direct_retries[slot_idx].retry_attempts_sent = 0; @@ -721,7 +757,6 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { _direct_retries[slot_idx].expect_path_growth = expect_path_growth; _direct_retries[slot_idx].queued = false; _direct_retries[slot_idx].active = true; - onDirectRetryEvent("armed", packet, retry_delay); } Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) { diff --git a/src/Mesh.h b/src/Mesh.h index ad4d8a2f53..422b79aba2 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -32,6 +32,8 @@ class Mesh : public Dispatcher { struct DirectRetryEntry { Packet* packet; Packet* trigger_packet; + unsigned long retry_started_at; + unsigned long echo_wait_started_at; unsigned long retry_at; uint32_t retry_delay; uint8_t retry_attempts_sent; @@ -115,7 +117,7 @@ class Mesh : public Dispatcher { /** * \returns delay before a specific retry attempt, where attempt_idx=0 is the first retry. */ - virtual uint32_t getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) const; + virtual uint32_t getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx); /** * \returns number of extra (Direct) ACK transmissions wanted. @@ -125,7 +127,7 @@ class Mesh : public Dispatcher { /** * \brief Optional hook for logging direct-retry lifecycle events. */ - virtual void onDirectRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis) { } + virtual void onDirectRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { } /** * \brief Perform search of local DB of peers/contacts. diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 357da2105a..dc206516f1 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -3,6 +3,7 @@ #include "TxtDataHelpers.h" #include "AdvertDataHelpers.h" #include +#include #define STR_HELPER(x) #x #define STR(x) STR_HELPER(x) @@ -14,17 +15,21 @@ #define DIRECT_RETRY_PREFS_MAGIC_0 0xD4 #define DIRECT_RETRY_PREFS_MAGIC_1 0x52 #define DIRECT_RETRY_RECENT_DEFAULT 1 -#define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT_X4 10 +#define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT_X4 DIRECT_RETRY_ROOFTOP_MARGIN_X4 #define DIRECT_RETRY_SNR_MARGIN_DB_MAX 40 #define DIRECT_RETRY_SNR_MARGIN_X4_MAX (DIRECT_RETRY_SNR_MARGIN_DB_MAX * 4) #define DIRECT_RETRY_TIMING_MAGIC_0 0xD5 #define DIRECT_RETRY_TIMING_MAGIC_1 0x54 -#define DIRECT_RETRY_COUNT_DEFAULT 15 +#define DIRECT_RETRY_COUNT_DEFAULT DIRECT_RETRY_ROOFTOP_COUNT #define DIRECT_RETRY_COUNT_MIN 1 #define DIRECT_RETRY_COUNT_MAX 15 -#define DIRECT_RETRY_BASE_MS_DEFAULT 200 +#define DIRECT_RETRY_BASE_MS_DEFAULT DIRECT_RETRY_ROOFTOP_BASE_MS #define DIRECT_RETRY_BASE_MS_MIN 10 #define DIRECT_RETRY_BASE_MS_MAX 5000 +#define DIRECT_RETRY_STEP_MS_DEFAULT DIRECT_RETRY_ROOFTOP_STEP_MS +#define DIRECT_RETRY_STEP_MS_MIN 0 +#define DIRECT_RETRY_STEP_MS_MAX 5000 +#define DIRECT_RETRY_PRESET_DEFAULT DIRECT_RETRY_PRESET_ROOFTOP // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { @@ -45,6 +50,98 @@ static float directRetryMarginX4ToDb(uint8_t margin_x4) { return ((float)margin_x4) / 4.0f; } +static uint8_t directRetryPresetOrDefault(uint8_t preset) { + if (preset <= DIRECT_RETRY_PRESET_MOBILE) { + return preset; + } + return DIRECT_RETRY_PRESET_DEFAULT; +} + +static const char* directRetryPresetName(uint8_t preset) { + switch (directRetryPresetOrDefault(preset)) { + case DIRECT_RETRY_PRESET_INFRA: + return "infra"; + case DIRECT_RETRY_PRESET_MOBILE: + return "mobile"; + case DIRECT_RETRY_PRESET_ROOFTOP: + default: + return "rooftop"; + } +} + +static uint8_t directRetryEffectiveCount(const NodePrefs* prefs) { + return constrain(prefs->direct_retry_attempts, (uint8_t)DIRECT_RETRY_COUNT_MIN, (uint8_t)DIRECT_RETRY_COUNT_MAX); +} + +static uint16_t directRetryEffectiveBaseMs(const NodePrefs* prefs) { + return constrain(prefs->direct_retry_base_ms, (uint16_t)DIRECT_RETRY_BASE_MS_MIN, (uint16_t)DIRECT_RETRY_BASE_MS_MAX); +} + +static uint16_t directRetryEffectiveStepMs(const NodePrefs* prefs) { + return constrain(prefs->direct_retry_step_ms, (uint16_t)DIRECT_RETRY_STEP_MS_MIN, (uint16_t)DIRECT_RETRY_STEP_MS_MAX); +} + +static uint8_t directRetryEffectiveMarginX4(const NodePrefs* prefs) { + return constrain(prefs->direct_retry_snr_margin_db, (uint8_t)0, (uint8_t)DIRECT_RETRY_SNR_MARGIN_X4_MAX); +} + +static uint16_t directRetryPresetStepDefault(uint8_t preset) { + switch (directRetryPresetOrDefault(preset)) { + case DIRECT_RETRY_PRESET_INFRA: + return DIRECT_RETRY_INFRA_STEP_MS; + case DIRECT_RETRY_PRESET_MOBILE: + return DIRECT_RETRY_MOBILE_STEP_MS; + case DIRECT_RETRY_PRESET_ROOFTOP: + default: + return DIRECT_RETRY_ROOFTOP_STEP_MS; + } +} + +static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) { + prefs->direct_retry_preset = directRetryPresetOrDefault(preset); + switch (prefs->direct_retry_preset) { + case DIRECT_RETRY_PRESET_INFRA: + prefs->direct_retry_base_ms = DIRECT_RETRY_INFRA_BASE_MS; + prefs->direct_retry_attempts = DIRECT_RETRY_INFRA_COUNT; + prefs->direct_retry_step_ms = DIRECT_RETRY_INFRA_STEP_MS; + prefs->direct_retry_snr_margin_db = DIRECT_RETRY_INFRA_MARGIN_X4; + break; + case DIRECT_RETRY_PRESET_MOBILE: + prefs->direct_retry_base_ms = DIRECT_RETRY_MOBILE_BASE_MS; + prefs->direct_retry_attempts = DIRECT_RETRY_MOBILE_COUNT; + prefs->direct_retry_step_ms = DIRECT_RETRY_MOBILE_STEP_MS; + prefs->direct_retry_snr_margin_db = DIRECT_RETRY_MOBILE_MARGIN_X4; + break; + case DIRECT_RETRY_PRESET_ROOFTOP: + default: + prefs->direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS; + prefs->direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT; + prefs->direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS; + prefs->direct_retry_snr_margin_db = DIRECT_RETRY_ROOFTOP_MARGIN_X4; + break; + } +} + +static bool parseDirectRetryPreset(const char* value, uint8_t& preset) { + if (value == NULL) { + return false; + } + if (strcmp(value, "0") == 0 || strcmp(value, "infra") == 0 + || strcmp(value, "infa") == 0 || strcmp(value, "infrastructure") == 0) { + preset = DIRECT_RETRY_PRESET_INFRA; + return true; + } + if (strcmp(value, "1") == 0 || strcmp(value, "rooftop") == 0) { + preset = DIRECT_RETRY_PRESET_ROOFTOP; + return true; + } + if (strcmp(value, "2") == 0 || strcmp(value, "mobile") == 0) { + preset = DIRECT_RETRY_PRESET_MOBILE; + return true; + } + return false; +} + static bool isValidName(const char *n) { while (*n) { if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false; @@ -127,6 +224,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->direct_retry_timing_magic[0], sizeof(_prefs->direct_retry_timing_magic)); // 294 size_t radio_fem_rxgain_read = file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 296 + file.read((uint8_t *)&_prefs->direct_retry_preset, sizeof(_prefs->direct_retry_preset)); // 297 + size_t retry_step_read = file.read((uint8_t *)&_prefs->direct_retry_step_ms, + sizeof(_prefs->direct_retry_step_ms)); // 298 // PowerSaving-only prefs stored radio_fem_rxgain at 291, before direct retry timing existed. if (radio_fem_rxgain_read != sizeof(_prefs->radio_fem_rxgain) && legacy_retry_attempts_read == sizeof(legacy_retry_attempts_or_radio_fem_rxgain) @@ -134,7 +234,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1)) { _prefs->radio_fem_rxgain = constrain(legacy_retry_attempts_or_radio_fem_rxgain, 0, 1); } - // next: 297 + // next: 298 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -182,6 +282,12 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // sanitise settings _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean _prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean + _prefs->direct_retry_preset = directRetryPresetOrDefault(_prefs->direct_retry_preset); + if (retry_step_read != sizeof(_prefs->direct_retry_step_ms)) { + _prefs->direct_retry_step_ms = directRetryPresetStepDefault(_prefs->direct_retry_preset); + } else { + _prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); + } file.close(); } @@ -252,7 +358,9 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { uint8_t retry_timing_magic[2] = { DIRECT_RETRY_TIMING_MAGIC_0, DIRECT_RETRY_TIMING_MAGIC_1 }; file.write(retry_timing_magic, sizeof(retry_timing_magic)); // 294 file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 296 - // next: 297 + file.write((uint8_t *)&_prefs->direct_retry_preset, sizeof(_prefs->direct_retry_preset)); // 297 + file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 298 + // next: 300 file.close(); } @@ -413,11 +521,17 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else if (memcmp(config, "direct.retry.heard", 18) == 0) { sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); } else if (memcmp(config, "direct.retry.margin", 19) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(directRetryMarginX4ToDb(_prefs->direct_retry_snr_margin_db))); + sprintf(reply, "> %s", StrHelper::ftoa(directRetryMarginX4ToDb(directRetryEffectiveMarginX4(_prefs)))); + } else if (memcmp(config, "direct.retry.preset", 19) == 0) { + sprintf(reply, "> %d,%s", + (uint32_t)directRetryPresetOrDefault(_prefs->direct_retry_preset), + directRetryPresetName(_prefs->direct_retry_preset)); } else if (memcmp(config, "direct.retry.count", 18) == 0) { - sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts); + sprintf(reply, "> %d", (uint32_t)directRetryEffectiveCount(_prefs)); } else if (memcmp(config, "direct.retry.base", 17) == 0) { - sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_base_ms); + sprintf(reply, "> %d", (uint32_t)directRetryEffectiveBaseMs(_prefs)); + } else if (memcmp(config, "direct.retry.step", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)directRetryEffectiveStepMs(_prefs)); } else if (memcmp(config, "owner.info", 10) == 0) { *reply++ = '>'; *reply++ = ' '; @@ -688,6 +802,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { sprintf(reply, "Error, min 0 and max %d", DIRECT_RETRY_SNR_MARGIN_DB_MAX); } + } else if (memcmp(config, "direct.retry.preset ", 20) == 0) { + uint8_t preset; + if (parseDirectRetryPreset(&config[20], preset)) { + applyDirectRetryPreset(_prefs, preset); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be infra, rooftop, mobile, 0, 1, or 2"); + } } else if (memcmp(config, "direct.retry.count ", 19) == 0) { int count = atoi(&config[19]); if (count >= DIRECT_RETRY_COUNT_MIN && count <= DIRECT_RETRY_COUNT_MAX) { @@ -706,6 +829,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_BASE_MS_MIN, DIRECT_RETRY_BASE_MS_MAX); } + } else if (memcmp(config, "direct.retry.step ", 18) == 0) { + int delay_ms = atoi(&config[18]); + if (delay_ms >= DIRECT_RETRY_STEP_MS_MIN && delay_ms <= DIRECT_RETRY_STEP_MS_MAX) { + _prefs->direct_retry_step_ms = (uint16_t)delay_ms; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; @@ -1224,6 +1356,15 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { sprintf(reply, "Error, min 0 and max %d", DIRECT_RETRY_SNR_MARGIN_DB_MAX); } + } else if (memcmp(config, "direct.retry.preset ", 20) == 0) { + uint8_t preset; + if (parseDirectRetryPreset(&config[20], preset)) { + applyDirectRetryPreset(_prefs, preset); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be infra, rooftop, mobile, 0, 1, or 2"); + } } else if (memcmp(config, "direct.retry.count ", 19) == 0) { int count = atoi(&config[19]); if (count >= DIRECT_RETRY_COUNT_MIN && count <= DIRECT_RETRY_COUNT_MAX) { @@ -1242,6 +1383,15 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_BASE_MS_MIN, DIRECT_RETRY_BASE_MS_MAX); } + } else if (memcmp(config, "direct.retry.step ", 18) == 0) { + int delay_ms = atoi(&config[18]); + if (delay_ms >= DIRECT_RETRY_STEP_MS_MIN && delay_ms <= DIRECT_RETRY_STEP_MS_MAX) { + _prefs->direct_retry_step_ms = (uint16_t)delay_ms; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; @@ -1420,11 +1570,17 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else if (memcmp(config, "direct.retry.heard", 18) == 0) { sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); } else if (memcmp(config, "direct.retry.margin", 19) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(directRetryMarginX4ToDb(_prefs->direct_retry_snr_margin_db))); + sprintf(reply, "> %s", StrHelper::ftoa(directRetryMarginX4ToDb(directRetryEffectiveMarginX4(_prefs)))); + } else if (memcmp(config, "direct.retry.preset", 19) == 0) { + sprintf(reply, "> %d,%s", + (uint32_t)directRetryPresetOrDefault(_prefs->direct_retry_preset), + directRetryPresetName(_prefs->direct_retry_preset)); } else if (memcmp(config, "direct.retry.count", 18) == 0) { - sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts); + sprintf(reply, "> %d", (uint32_t)directRetryEffectiveCount(_prefs)); } else if (memcmp(config, "direct.retry.base", 17) == 0) { - sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_base_ms); + sprintf(reply, "> %d", (uint32_t)directRetryEffectiveBaseMs(_prefs)); + } else if (memcmp(config, "direct.retry.step", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)directRetryEffectiveStepMs(_prefs)); } else if (memcmp(config, "owner.info", 10) == 0) { *reply++ = '>'; *reply++ = ' '; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 3ca193578d..ea30777a2d 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -19,6 +19,25 @@ #define LOOP_DETECT_MODERATE 2 #define LOOP_DETECT_STRICT 3 +#define DIRECT_RETRY_PRESET_INFRA 0 +#define DIRECT_RETRY_PRESET_ROOFTOP 1 +#define DIRECT_RETRY_PRESET_MOBILE 2 + +#define DIRECT_RETRY_INFRA_BASE_MS 275 +#define DIRECT_RETRY_INFRA_COUNT 4 +#define DIRECT_RETRY_INFRA_STEP_MS 150 +#define DIRECT_RETRY_INFRA_MARGIN_X4 60 + +#define DIRECT_RETRY_ROOFTOP_BASE_MS 175 +#define DIRECT_RETRY_ROOFTOP_COUNT 15 +#define DIRECT_RETRY_ROOFTOP_STEP_MS 100 +#define DIRECT_RETRY_ROOFTOP_MARGIN_X4 20 + +#define DIRECT_RETRY_MOBILE_BASE_MS 175 +#define DIRECT_RETRY_MOBILE_COUNT 15 +#define DIRECT_RETRY_MOBILE_STEP_MS 50 +#define DIRECT_RETRY_MOBILE_MARGIN_X4 0 + struct NodePrefs { // persisted to file float airtime_factor; char node_name[32]; @@ -67,6 +86,8 @@ struct NodePrefs { // persisted to file uint8_t direct_retry_attempts; uint16_t direct_retry_base_ms; uint8_t direct_retry_timing_magic[2]; + uint8_t direct_retry_preset; + uint16_t direct_retry_step_ms; }; class CommonCLICallbacks { diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index ac6d01b57b..ae28acc8fd 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -9,8 +9,10 @@ #define MAX_PACKET_HASHES 128 #define MAX_PACKET_ACKS 64 #ifndef MAX_RECENT_REPEATERS - // Two defaults. Can be overridden with -D MAX_RECENT_REPEATERS=. - #if defined(ESP32) + // Platform defaults. Can be overridden with -D MAX_RECENT_REPEATERS=. + #if defined(ESP32) || defined(ESP32_PLATFORM) + #define MAX_RECENT_REPEATERS 2048 + #elif defined(NRF52_PLATFORM) #define MAX_RECENT_REPEATERS 512 #else #define MAX_RECENT_REPEATERS 64 @@ -23,9 +25,7 @@ class SimpleMeshTables : public mesh::MeshTables { typedef bool (*RecentRepeaterAllowFn)(const uint8_t* prefix, uint8_t prefix_len, void* ctx); struct RecentRepeaterInfo { - // Just enough identity to match a next-hop path prefix plus the SNR that heard it. - uint16_t retry_count; - uint16_t fail_count; + // Identity and link quality for a next-hop path prefix. uint8_t prefix[MAX_ROUTE_HASH_BYTES]; uint8_t prefix_len; int8_t snr_x4; @@ -80,8 +80,8 @@ class SimpleMeshTables : public mesh::MeshTables { int8_t weightedSnrX4RoundUp(int8_t curr_snr_x4, int8_t new_snr_x4) const { // Keep existing SNR heavier than a single new sample: 75% existing + 25% new. - int16_t weighted_sum = ((int16_t)curr_snr_x4 * 3) + (int16_t)new_snr_x4; - int16_t blended = weighted_sum / 4; // truncates toward zero + int32_t weighted_sum = ((int32_t)curr_snr_x4 * 3) + (int32_t)new_snr_x4; + int32_t blended = weighted_sum / 4; // truncates toward zero // "Round up" means ceil(), which only differs from truncation for positive remainders. if (weighted_sum > 0 && (weighted_sum % 4) != 0) { blended++; @@ -160,8 +160,11 @@ class SimpleMeshTables : public mesh::MeshTables { f.read((uint8_t *) &_next_idx, sizeof(_next_idx)); f.read((uint8_t *) &_acks[0], sizeof(_acks)); f.read((uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx)); - f.read((uint8_t *) &_recent_repeaters[0], sizeof(_recent_repeaters)); - f.read((uint8_t *) &_next_recent_repeater_idx, sizeof(_next_recent_repeater_idx)); + // Recent repeater entries are intentionally not restored across boots. + // This avoids struct-layout migration issues and keeps stale path quality + // stats from persisting indefinitely. + memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); + _next_recent_repeater_idx = 0; } void saveTo(File f) { f.write(_hashes, sizeof(_hashes)); @@ -321,96 +324,10 @@ class SimpleMeshTables : public mesh::MeshTables { memcpy(slot.prefix, prefix, prefix_len); slot.prefix_len = prefix_len; slot.snr_x4 = snr_x4; - slot.retry_count = 0; - slot.fail_count = 0; slot.snr_locked = snr_locked ? 1 : 0; _next_recent_repeater_idx = (slot_idx + 1) % MAX_RECENT_REPEATERS; return true; } - bool incrementRecentRepeaterRetryCount(const uint8_t* prefix, uint8_t prefix_len, - bool create_if_missing = false, int8_t seed_snr_x4 = 0, - bool bypass_allow_filter = false) { - if (prefix == NULL || prefix_len == 0) { - return false; - } - if (prefix_len > MAX_ROUTE_HASH_BYTES) { - prefix_len = MAX_ROUTE_HASH_BYTES; - } - - for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { - RecentRepeaterInfo& existing = _recent_repeaters[i]; - if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { - continue; - } - if (prefix_len > existing.prefix_len) { - memset(existing.prefix, 0, sizeof(existing.prefix)); - memcpy(existing.prefix, prefix, prefix_len); - existing.prefix_len = prefix_len; - } - if (existing.retry_count < 0xFFFF) { - existing.retry_count++; - } - return true; - } - - if (!create_if_missing || !setRecentRepeater(prefix, prefix_len, seed_snr_x4, false, bypass_allow_filter)) { - return false; - } - - for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { - RecentRepeaterInfo& existing = _recent_repeaters[i]; - if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { - continue; - } - if (existing.retry_count < 0xFFFF) { - existing.retry_count++; - } - return true; - } - return false; - } - bool incrementRecentRepeaterFailCount(const uint8_t* prefix, uint8_t prefix_len, - bool create_if_missing = false, int8_t seed_snr_x4 = 0, - bool bypass_allow_filter = false) { - if (prefix == NULL || prefix_len == 0) { - return false; - } - if (prefix_len > MAX_ROUTE_HASH_BYTES) { - prefix_len = MAX_ROUTE_HASH_BYTES; - } - - for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { - RecentRepeaterInfo& existing = _recent_repeaters[i]; - if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { - continue; - } - if (prefix_len > existing.prefix_len) { - memset(existing.prefix, 0, sizeof(existing.prefix)); - memcpy(existing.prefix, prefix, prefix_len); - existing.prefix_len = prefix_len; - } - if (existing.fail_count < 0xFFFF) { - existing.fail_count++; - } - return true; - } - - if (!create_if_missing || !setRecentRepeater(prefix, prefix_len, seed_snr_x4, false, bypass_allow_filter)) { - return false; - } - - for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { - RecentRepeaterInfo& existing = _recent_repeaters[i]; - if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { - continue; - } - if (existing.fail_count < 0xFFFF) { - existing.fail_count++; - } - return true; - } - return false; - } bool decrementRecentRepeaterSnrX4(const uint8_t* prefix, uint8_t prefix_len, uint8_t amount_x4 = 1) { if (prefix == NULL || prefix_len == 0 || amount_x4 == 0) { return false; From 4b49449ff6e0cad9c9e5c606794574beae83a667 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Thu, 7 May 2026 20:43:36 +0700 Subject: [PATCH 25/94] fix CustomLFS version pinning --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 864e5e1ffe..2a6c23dfae 100644 --- a/platformio.ini +++ b/platformio.ini @@ -91,7 +91,7 @@ build_flags = ${arduino_base.build_flags} -D EXTRAFS=1 lib_deps = ${arduino_base.lib_deps} - https://github.com/oltaco/CustomLFS @ 0.2.1 + https://github.com/oltaco/CustomLFS#0.2.1 ; ----------------- RP2040 --------------------- [rp2040_base] From 041f103b019701eb4d02609693edd66ab878fa06 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Thu, 7 May 2026 21:09:38 +0700 Subject: [PATCH 26/94] Reduced MAX_CONTACTS to 100 to compile and add PS BLE --- variants/lilygo_tlora_v2_1/platformio.ini | 25 ++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index 3641f12705..1b0cb6ec00 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -92,7 +92,30 @@ extends = LilyGo_TLora_V2_1_1_6 build_flags = ${LilyGo_TLora_V2_1_1_6.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=160 + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_TLora_V2_1_1_6.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:LilyGo_TLora_V2_1_1_6_companion_radio_ble_ps] +extends = LilyGo_TLora_V2_1_1_6 +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${LilyGo_TLora_V2_1_1_6.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 -D OFFLINE_QUEUE_SIZE=256 From cc1759aa42a7e1a45cc4477017e4d1ae094be893 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Thu, 7 May 2026 21:11:56 +0700 Subject: [PATCH 27/94] Added PS BLE --- variants/heltec_ct62/platformio.ini | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/variants/heltec_ct62/platformio.ini b/variants/heltec_ct62/platformio.ini index 910385ecdf..0031c8ef53 100644 --- a/variants/heltec_ct62/platformio.ini +++ b/variants/heltec_ct62/platformio.ini @@ -130,6 +130,28 @@ lib_deps = ${esp32_ota.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_ct62_companion_radio_ble_ps] +extends = Heltec_ct62 +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${Heltec_ct62.build_flags} +; -D ARDUINO_USB_MODE=1 +; -D ARDUINO_USB_CDC_ON_BOOT=1 + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D BLE_PIN_CODE=123456 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_ct62.build_src_filter} + +<../examples/companion_radio/*.cpp> + + +lib_deps = + ${Heltec_ct62.lib_deps} + ${esp32_ota.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Heltec_ct62_sensor] extends = Heltec_ct62 build_flags = From 1610c0a19b08feb982078155aadbf62b7b104b85 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Thu, 7 May 2026 21:13:50 +0700 Subject: [PATCH 28/94] Added more boards to PowerSaving --- build-iotthinks.sh | 70 ++++++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/build-iotthinks.sh b/build-iotthinks.sh index 7c654482c7..9bff2eff42 100644 --- a/build-iotthinks.sh +++ b/build-iotthinks.sh @@ -1,9 +1,9 @@ # sh ./build-repeaters-iotthinks.sh -export FIRMWARE_VERSION="PowerSaving15" +export FIRMWARE_VERSION="PowerSaving15.0.2" ############# Repeaters ############# # Commonly-used boards -## ESP32 - 12 boards +## ESP32 - 17 boards sh build.sh build-firmware \ Heltec_v3_repeater \ Heltec_WSL3_repeater \ @@ -16,9 +16,14 @@ Xiao_S3_WIO_repeater \ Xiao_C3_repeater \ Xiao_C6_repeater_ \ Heltec_E290_repeater \ -Heltec_Wireless_Tracker_repeater +Heltec_Wireless_Tracker_repeater \ +LilyGo_TBeam_1W_repeater \ +Xiao_S3_repeater \ +heltec_tracker_v2_repeater \ +Heltec_Wireless_Paper_repeater \ +Heltec_ct62_repeater -## NRF52 - 13 boards +## NRF52 - 17 boards sh build.sh build-firmware \ RAK_4631_repeater \ Heltec_t114_repeater \ @@ -32,7 +37,12 @@ WioTrackerL1_repeater \ RAK_3401_repeater \ RAK_WisMesh_Tag_repeater \ GAT562_30S_Mesh_Kit_repeater \ -GAT562_Mesh_Tracker_Pro_repeater +GAT562_Mesh_Tracker_Pro_repeater \ +ikoka_nano_nrf_22dbm_repeater \ +ikoka_nano_nrf_30dbm_repeater \ +ikoka_nano_nrf_33dbm_repeater \ +ThinkNode_M1_repeater \ +Heltec_t096_repeater ## ESP32, SX1276 - 3 boards sh build.sh build-firmware \ @@ -40,37 +50,29 @@ Heltec_v2_repeater \ LilyGo_TLora_V2_1_1_6_repeater \ Tbeam_SX1276_repeater -## Ikoka - 3 boards -sh build.sh build-firmware \ -ikoka_nano_nrf_22dbm_repeater \ -ikoka_nano_nrf_30dbm_repeater \ -ikoka_nano_nrf_33dbm_repeater - ############# Room Server ############# -# ESP32 +# ESP32 - 7 boards sh build.sh build-firmware \ Heltec_v3_room_server \ -heltec_v4_room_server +heltec_v4_room_server \ +LilyGo_TBeam_1W_room_server \ +Heltec_WSL3_room_server \ +Xiao_S3_room_server \ +heltec_tracker_v2_room_server \ +Heltec_Wireless_Paper_room_server -# NRF52 +# NRF52 - 6 boards sh build.sh build-firmware \ RAK_4631_room_server \ Heltec_t114_room_server \ Xiao_nrf52_room_server \ t1000e_room_server \ WioTrackerL1_room_server \ -RAK_3401_room_server +RAK_3401_room_server \ +Heltec_t096_room_server ############# Companions BLE ############# -# ESP32 -sh build.sh build-firmware \ -Heltec_v3_companion_radio_ble_ps \ -heltec_v4_companion_radio_ble_ps \ -heltec_v4_companion_radio_ble_ps_femoff \ -Xiao_S3_WIO_companion_radio_ble \ -Heltec_Wireless_Paper_companion_radio_ble - -# NRF52 +# NRF52 - 12 boards sh build.sh build-firmware \ RAK_4631_companion_radio_ble \ Heltec_t114_companion_radio_ble \ @@ -79,13 +81,14 @@ t1000e_companion_radio_ble \ LilyGo_T-Echo_companion_radio_ble \ WioTrackerL1_companion_radio_ble \ RAK_3401_companion_radio_ble \ -RAK_WisMesh_Tag_companion_radio_ble - -############# Companions USB ############# -sh build.sh build-firmware \ -Heltec_v3_companion_radio_usb +RAK_WisMesh_Tag_companion_radio_ble \ +SenseCap_Solar_companion_radio_ble \ +ThinkNode_M1_companion_radio_ble \ +Heltec_t096_companion_radio_ble \ +Heltec_t096_companion_radio_ble_femoff ############# Companions BLE PS ############# +# ESP32 - 13 boards sh build.sh build-firmware \ Heltec_v3_companion_radio_ble_ps \ heltec_v4_companion_radio_ble_ps \ @@ -93,4 +96,11 @@ heltec_v4_3_companion_radio_ble_ps_femoff \ Xiao_C3_companion_radio_ble_ps \ Xiao_S3_companion_radio_ble_ps \ Xiao_S3_WIO_companion_radio_ble_ps \ -Heltec_v2_companion_radio_ble_ps +Heltec_v2_companion_radio_ble_ps \ +LilyGo_TBeam_1W_companion_radio_ble_ps \ +Heltec_WSL3_companion_radio_ble_ps \ +Heltec_Wireless_Tracker_companion_radio_ble_ps \ +heltec_tracker_v2_companion_radio_ble_ps \ +Heltec_Wireless_Paper_companion_radio_ble_ps \ +LilyGo_TLora_V2_1_1_6_companion_radio_ble_ps \ +Heltec_ct62_companion_radio_ble_ps From d4febe7867938fc7a38898db4fecd5fe1be55edc Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 7 May 2026 11:48:29 -0700 Subject: [PATCH 29/94] Add flood retry controls --- docs/cli_commands.md | 88 +++++- examples/simple_repeater/MyMesh.cpp | 293 +++++++++++++++++ examples/simple_repeater/MyMesh.h | 24 ++ src/Dispatcher.cpp | 25 +- src/Dispatcher.h | 5 +- src/Mesh.cpp | 280 ++++++++++++++++- src/Mesh.h | 64 ++++ src/helpers/CommonCLI.cpp | 401 +++++++++++++++++++++++- src/helpers/CommonCLI.h | 19 ++ src/helpers/SimpleMeshTables.h | 16 + src/helpers/StaticPoolPacketManager.cpp | 5 +- src/helpers/StaticPoolPacketManager.h | 4 +- 12 files changed, 1204 insertions(+), 20 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index f9c0825976..79d319adfe 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -585,7 +585,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `rooftop` (`1`): `175 ms` base wait, `15` retries, `100 ms` added per retry, SNR gate is SF floor + `5 dB` - `mobile` (`2`): `175 ms` base wait, `15` retries, `50 ms` added per retry, SNR gate is the SF floor -**Note:** Selecting a preset copies those values into the retry settings. You can refine `direct.retry.margin`, `direct.retry.count`, `direct.retry.base`, or `direct.retry.step` afterward. Retry delay is `direct.txdelay` jitter + base wait + packet-length airtime wait + per-attempt step. +**Note:** Selecting a preset copies those values into the direct retry settings and also resets flood retry defaults. You can refine `direct.retry.margin`, `direct.retry.count`, `direct.retry.base`, `direct.retry.step`, `flood.retry.count`, or `flood.retry.path` afterward. Retry delay is `direct.txdelay` jitter + base wait + packet-length airtime wait + per-attempt step. --- @@ -754,6 +754,92 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change the flood retry preset +**Usage:** +- `get flood.retry.preset` +- `set flood.retry.preset ` + +**Parameters:** +- `value`: `infra`|`rooftop`|`mobile` or `0`|`1`|`2` + +**Presets:** +- `infra` (`0`): `1` retry, path gate `1` +- `rooftop` (`1`): `3` retries, path gate `2` +- `mobile` (`2`): `3` retries, path gate `1` + +**Note:** This applies only the flood retry defaults. `set direct.retry.preset` also resets these flood retry defaults. + +--- + +#### View or change the number of flood retry attempts +**Usage:** +- `get flood.retry.count` +- `set flood.retry.count ` + +**Parameters:** +- `value`: Maximum retry attempts after initial flood TX (`0`-`3`) + +**Default:** `3` for `rooftop` and `mobile`, `1` for `infra` + +**Note:** `0` disables flood retry. + +--- + +#### View or change the flood retry path gate +**Usage:** +- `get flood.retry.path` +- `set flood.retry.path ` + +**Parameters:** +- `value`: Maximum flood path length eligible for retry (`0`-`63`), or `off` to disable the gate + +**Default:** `2` for `rooftop`, `1` for `infra` and `mobile` + +--- + +#### View or change flood retry target prefixes +**Usage:** +- `get flood.retry.prefixes` +- `set flood.retry.prefixes ` + +**Parameters:** +- `prefixes`: Comma-separated 3-byte hex prefixes, such as `A1B2C3,D4E5F6`; use `none` or `off` to clear + +**Default:** `none` + +**Note:** Prefixes are stored as 3 bytes. Flood retry skips packets whose path already contains a matching target prefix. When prefixes are configured, only a downstream echo from one of those target prefixes cancels a queued retry; when no prefixes are configured, any downstream echo cancels it. Matching works with 3-byte, 2-byte, or 1-byte flood paths by comparing the matching leading bytes. + +--- + +#### View or change flood retry bridge mode +**Usage:** +- `get flood.retry.bridge` +- `set flood.retry.bridge ` + +**Parameters:** +- `state`: `on` or `off` + +**Default:** `off` + +**Note:** Bridge mode uses bucket definitions instead of the single `flood.retry.prefixes` target list. If a flood comes from one fresh bucket, retry continues until every other fresh configured bucket has been heard or `flood.retry.count` is exhausted. + +--- + +#### View or change flood retry bridge buckets +**Usage:** +- `get flood.retry.bucket.` +- `set flood.retry.bucket ` + +**Parameters:** +- `bucket`: Bucket number (`1`-`6`) +- `prefixes`: Up to 8 comma-separated 3-byte hex prefixes, such as `AABBCC,223344`; use `none` or `off` to clear + +**Default:** all buckets empty + +**Note:** Prefixes are stored as 3 bytes but match 3-byte, 2-byte, and 1-byte flood paths by comparing leading bytes. Bucket prefixes are included in bridge retry logic only if they were heard in the recent repeater table within the last hour. + +--- + ### ACL #### Add, update or remove permissions for a companion diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 6b3e8d6a7d..404a26d606 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -854,6 +854,180 @@ uint8_t MyMesh::getDirectRetryConfiguredMaxAttempts() const { uint32_t MyMesh::getDirectRetryAttemptStepMillis() const { return constrain((uint32_t)_prefs.direct_retry_step_ms, (uint32_t)0, (uint32_t)5000); } +bool MyMesh::hasFloodRetryPrefixes() const { + for (int i = 0; i < FLOOD_RETRY_PREFIX_SLOTS; i++) { + const uint8_t* configured = _prefs.flood_retry_prefixes[i]; + if (configured[0] != 0 || configured[1] != 0 || configured[2] != 0) { + return true; + } + } + return false; +} +bool MyMesh::floodRetryLastHopMatches(const mesh::Packet* packet) const { + if (packet == NULL || packet->getPathHashCount() == 0) { + return false; + } + + uint8_t hash_size = packet->getPathHashSize(); + if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { + return false; + } + + const uint8_t* heard_prefix = &packet->path[(packet->getPathHashCount() - 1) * hash_size]; + for (int i = 0; i < FLOOD_RETRY_PREFIX_SLOTS; i++) { + const uint8_t* configured = _prefs.flood_retry_prefixes[i]; + if ((configured[0] != 0 || configured[1] != 0 || configured[2] != 0) + && memcmp(configured, heard_prefix, hash_size) == 0) { + return true; + } + } + + return false; +} +bool MyMesh::floodRetryPrefixMatches(const mesh::Packet* packet) const { + if (packet == NULL || packet->getPathHashCount() == 0) { + return false; + } + + uint8_t hash_size = packet->getPathHashSize(); + if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { + return false; + } + + const uint8_t* path = packet->path; + for (int hop = 0; hop < packet->getPathHashCount(); hop++) { + for (int i = 0; i < FLOOD_RETRY_PREFIX_SLOTS; i++) { + const uint8_t* configured = _prefs.flood_retry_prefixes[i]; + if ((configured[0] != 0 || configured[1] != 0 || configured[2] != 0) + && memcmp(configured, path, hash_size) == 0) { + return true; + } + } + path += hash_size; + } + + return false; +} +bool MyMesh::floodRetryPrefixFresh(const uint8_t* prefix, uint8_t prefix_len) const { + const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(prefix, prefix_len); + if (recent == NULL || recent->last_heard_millis == 0) { + return false; + } + return (uint32_t)(millis() - recent->last_heard_millis) <= 3600000UL; +} +int MyMesh::floodRetryBucketForPrefix(const uint8_t* prefix, uint8_t prefix_len, bool require_fresh) const { + if (prefix == NULL || prefix_len == 0 || prefix_len > MAX_ROUTE_HASH_BYTES) { + return -1; + } + if (require_fresh && !floodRetryPrefixFresh(prefix, prefix_len)) { + return -1; + } + for (int bucket = 0; bucket < FLOOD_RETRY_BRIDGE_BUCKETS; bucket++) { + for (int i = 0; i < FLOOD_RETRY_BUCKET_PREFIXES; i++) { + const uint8_t* configured = _prefs.flood_retry_bridge_buckets[bucket][i]; + if ((configured[0] != 0 || configured[1] != 0 || configured[2] != 0) + && memcmp(configured, prefix, prefix_len) == 0) { + return bucket; + } + } + } + return -1; +} +int MyMesh::floodRetrySourceBucket(const mesh::Packet* packet) const { + if (packet == NULL || packet->getPathHashCount() < 2) { + return -1; + } + uint8_t hash_size = packet->getPathHashSize(); + if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { + return -1; + } + const uint8_t* source_prefix = &packet->path[(packet->getPathHashCount() - 2) * hash_size]; + return floodRetryBucketForPrefix(source_prefix, hash_size, true); +} +uint8_t MyMesh::floodRetryBridgeTargetMask(uint8_t source_bucket) const { + uint8_t mask = 0; + for (int bucket = 0; bucket < FLOOD_RETRY_BRIDGE_BUCKETS; bucket++) { + if (bucket == source_bucket) { + continue; + } + for (int i = 0; i < FLOOD_RETRY_BUCKET_PREFIXES; i++) { + const uint8_t* configured = _prefs.flood_retry_bridge_buckets[bucket][i]; + if ((configured[0] != 0 || configured[1] != 0 || configured[2] != 0) + && floodRetryPrefixFresh(configured, FLOOD_RETRY_PREFIX_LEN)) { + mask |= (uint8_t)(1U << bucket); + break; + } + } + } + return mask; +} +uint8_t MyMesh::floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t source_bucket) const { + if (packet == NULL || packet->getPathHashCount() == 0) { + return 0; + } + uint8_t hash_size = packet->getPathHashSize(); + if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { + return 0; + } + + uint8_t mask = 0; + const uint8_t* path = packet->path; + for (int hop = 0; hop < packet->getPathHashCount(); hop++) { + int bucket = floodRetryBucketForPrefix(path, hash_size, true); + if (bucket >= 0 && bucket != source_bucket) { + mask |= (uint8_t)(1U << bucket); + } + path += hash_size; + } + return mask; +} +MyMesh::FloodRetryBridgeState* MyMesh::floodRetryBridgeStateFor(const mesh::Packet* packet, bool create) const { + if (packet == NULL) { + return NULL; + } + + uint8_t key[MAX_HASH_SIZE]; + packet->calculatePacketHash(key); + FloodRetryBridgeState* free_slot = NULL; + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + if (flood_retry_bridge_states[i].active + && memcmp(flood_retry_bridge_states[i].key, key, MAX_HASH_SIZE) == 0) { + return &flood_retry_bridge_states[i]; + } + if (!flood_retry_bridge_states[i].active && free_slot == NULL) { + free_slot = &flood_retry_bridge_states[i]; + } + } + if (!create) { + return NULL; + } + if (free_slot == NULL) { + return NULL; + } + + int source_bucket = floodRetrySourceBucket(packet); + if (source_bucket < 0) { + return NULL; + } + + uint8_t target_mask = floodRetryBridgeTargetMask((uint8_t)source_bucket); + if (target_mask == 0) { + return NULL; + } + + uint8_t heard_mask = floodRetryBridgeHeardMask(packet, (uint8_t)source_bucket) & target_mask; + if ((heard_mask & target_mask) == target_mask) { + return NULL; + } + + memset(free_slot, 0, sizeof(*free_slot)); + memcpy(free_slot->key, key, sizeof(free_slot->key)); + free_slot->source_bucket = (uint8_t)source_bucket; + free_slot->target_mask = target_mask; + free_slot->heard_mask = heard_mask; + free_slot->active = true; + return free_slot; +} uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { uint32_t base_wait_millis = constrain((uint32_t)_prefs.direct_retry_base_ms, (uint32_t)10, (uint32_t)5000); if (packet == NULL) { @@ -871,6 +1045,121 @@ uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { uint32_t scaled_wait_millis = (uint32_t) ((((float) bits) * 4.0f) / kbps); return base_wait_millis + scaled_wait_millis; } +bool MyMesh::allowFloodRetry(const mesh::Packet* packet) const { + if (_prefs.disable_fwd || constrain(_prefs.flood_retry_attempts, (uint8_t)0, (uint8_t)3) == 0) { + return false; + } + if (!_prefs.flood_retry_bridge_enabled) { + return true; + } + FloodRetryBridgeState* state = floodRetryBridgeStateFor(packet, true); + if (state == NULL) { + return false; + } + if ((state->heard_mask & state->target_mask) == state->target_mask) { + state->active = false; + return false; + } + return true; +} +void MyMesh::clearFloodRetryBridgeState(const mesh::Packet* packet) { + FloodRetryBridgeState* state = floodRetryBridgeStateFor(packet, false); + if (state != NULL) { + state->active = false; + } +} +void MyMesh::onFloodRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { + if (event == NULL || packet == NULL) { + return; + } + + const char* time_label = "time_ms"; + if (strcmp(event, "queued") == 0 || strcmp(event, "dropped_queue_full") == 0) { + time_label = "wait_ms"; + } else if (strcmp(event, "resent") == 0 || strcmp(event, "failed_all_tries") == 0 + || strcmp(event, "failure") == 0 || strncmp(event, "dropped_", 8) == 0) { + time_label = "elapsed_ms"; + } else if (strcmp(event, "good") == 0) { + time_label = "echo_ms"; + } + + MESH_DEBUG_PRINTLN("%s flood retry %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%u, %s=%lu)", + getLogDateTime(), + event, + (unsigned int)retry_attempt, + (uint32_t)packet->getPayloadType(), + packet->isRouteDirect() ? "D" : "F", + (uint32_t)packet->payload_len, + (unsigned int)packet->getPathHashCount(), + time_label, + (unsigned long)delay_millis); + + if (_logging) { + File f = openAppend(PACKET_LOG_FILE); + if (f) { + f.print(getLogDateTime()); + f.printf(": FLOOD RETRY %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%u, %s=%lu)\n", + event, + (unsigned int)retry_attempt, + (uint32_t)packet->getPayloadType(), + packet->isRouteDirect() ? "D" : "F", + (uint32_t)packet->payload_len, + (unsigned int)packet->getPathHashCount(), + time_label, + (unsigned long)delay_millis); + f.close(); + } + } + + if (_prefs.flood_retry_bridge_enabled + && (strcmp(event, "failure") == 0 || strcmp(event, "failed_all_tries") == 0 + || strncmp(event, "dropped_", 8) == 0)) { + clearFloodRetryBridgeState(packet); + } +} +bool MyMesh::hasFloodRetryTargetPrefix(const mesh::Packet* packet) const { + if (_prefs.flood_retry_bridge_enabled) { + return false; + } + return floodRetryPrefixMatches(packet); +} +uint8_t MyMesh::getFloodRetryMaxPathLength(const mesh::Packet* packet) const { + uint8_t gate = _prefs.flood_retry_path_gate; + if (gate == FLOOD_RETRY_PATH_GATE_DISABLED) { + return FLOOD_RETRY_PATH_GATE_DISABLED; + } + return gate <= 63 ? gate : 2; +} +uint8_t MyMesh::getFloodRetryMaxAttempts(const mesh::Packet* packet) const { + if (_prefs.disable_fwd) { + return 0; + } + return constrain(_prefs.flood_retry_attempts, (uint8_t)0, (uint8_t)3); +} +bool MyMesh::isFloodRetryEchoTarget(const mesh::Packet* packet, uint8_t progress_marker) const { + if (packet == NULL || !packet->isRouteFlood()) { + return false; + } + if (_prefs.flood_retry_bridge_enabled) { + FloodRetryBridgeState* state = floodRetryBridgeStateFor(packet, false); + if (state == NULL) { + return false; + } + state->heard_mask |= floodRetryBridgeHeardMask(packet, state->source_bucket) & state->target_mask; + bool complete = (state->heard_mask & state->target_mask) == state->target_mask; + if (complete) { + state->active = false; + } + return complete; + } + if (hasFloodRetryPrefixes()) { + return floodRetryLastHopMatches(packet); + } + if (packet->getPathHashCount() <= progress_marker) { + return false; + } + return true; +} uint8_t MyMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const { uint8_t configured_attempts = getDirectRetryConfiguredMaxAttempts(); uint8_t total_hops = 0; @@ -1222,6 +1511,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc set_radio_at = revert_radio_at = 0; _logging = false; region_load_active = false; + memset(flood_retry_bridge_states, 0, sizeof(flood_retry_bridge_states)); #if MAX_NEIGHBOURS memset(neighbours, 0, sizeof(neighbours)); @@ -1239,6 +1529,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS; _prefs.direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS; _prefs.direct_retry_preset = DIRECT_RETRY_PRESET_ROOFTOP; + _prefs.flood_retry_attempts = 3; + _prefs.flood_retry_path_gate = 2; + _prefs.flood_retry_bridge_enabled = 0; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; _prefs.node_lon = ADVERT_LON; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 1a2f505c5e..80c41b029c 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -99,6 +99,14 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { RegionEntry* recv_pkt_region; TransportKey default_scope; RateLimiter discover_limiter, anon_limiter; + struct FloodRetryBridgeState { + uint8_t key[MAX_HASH_SIZE]; + uint8_t source_bucket; + uint8_t target_mask; + uint8_t heard_mask; + bool active; + }; + mutable FloodRetryBridgeState flood_retry_bridge_states[MAX_FLOOD_RETRY_SLOTS]; uint32_t pending_discover_tag; unsigned long pending_discover_until; bool region_load_active; @@ -127,6 +135,16 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t getDirectRetryPreset() const; uint8_t getDirectRetryConfiguredMaxAttempts() const; uint32_t getDirectRetryAttemptStepMillis() const; + bool hasFloodRetryPrefixes() const; + bool floodRetryPrefixMatches(const mesh::Packet* packet) const; + bool floodRetryLastHopMatches(const mesh::Packet* packet) const; + bool floodRetryPrefixFresh(const uint8_t* prefix, uint8_t prefix_len) const; + int floodRetryBucketForPrefix(const uint8_t* prefix, uint8_t prefix_len, bool require_fresh) const; + int floodRetrySourceBucket(const mesh::Packet* packet) const; + uint8_t floodRetryBridgeTargetMask(uint8_t source_bucket) const; + uint8_t floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t source_bucket) const; + FloodRetryBridgeState* floodRetryBridgeStateFor(const mesh::Packet* packet, bool create) const; + void clearFloodRetryBridgeState(const mesh::Packet* packet); void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); @@ -159,6 +177,12 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override; uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override; void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override; + bool allowFloodRetry(const mesh::Packet* packet) const override; + void onFloodRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override; + bool hasFloodRetryTargetPrefix(const mesh::Packet* packet) const override; + uint8_t getFloodRetryMaxPathLength(const mesh::Packet* packet) const override; + uint8_t getFloodRetryMaxAttempts(const mesh::Packet* packet) const override; + bool isFloodRetryEchoTarget(const mesh::Packet* packet, uint8_t progress_marker) const override; int getInterferenceThreshold() const override { return _prefs.interference_threshold; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index cccbd36c79..a59f0be6fe 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -269,7 +269,10 @@ void Dispatcher::processRecvPacket(Packet* pkt) { uint8_t priority = (action >> 24) - 1; uint32_t _delay = action & 0xFFFFFF; - _mgr->queueOutbound(pkt, priority, futureMillis(_delay)); + if (!queueOutboundPacket(pkt, priority, _delay)) { + onSendFail(pkt); + releasePacket(pkt); + } } } @@ -320,7 +323,8 @@ void Dispatcher::checkSend() { if (len + outbound->payload_len > MAX_TRANS_UNIT) { MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): FATAL: Invalid packet queued... too long, len=%d", getLogDateTime(), len + outbound->payload_len); - _mgr->free(outbound); + onSendFail(outbound); + releasePacket(outbound); outbound = NULL; } else { memcpy(&raw[len], outbound->payload, outbound->payload_len); len += outbound->payload_len; @@ -332,6 +336,7 @@ void Dispatcher::checkSend() { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): ERROR: send start failed!", getLogDateTime()); logTxFail(outbound, outbound->getRawLength()); + onSendFail(outbound); releasePacket(outbound); // return to pool outbound = NULL; @@ -369,13 +374,21 @@ void Dispatcher::releasePacket(Packet* packet) { _mgr->free(packet); } -void Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) { +bool Dispatcher::queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) { if (!Packet::isValidPathLen(packet->path_len) || packet->payload_len > MAX_PACKET_PAYLOAD) { MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): ERROR: invalid packet... path_len=%d, payload_len=%d", getLogDateTime(), (uint32_t) packet->path_len, (uint32_t) packet->payload_len); - _mgr->free(packet); - } else { - _mgr->queueOutbound(packet, priority, futureMillis(delay_millis)); + return false; + } + return _mgr->queueOutbound(packet, priority, futureMillis(delay_millis)); +} + +bool Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) { + if (!queueOutboundPacket(packet, priority, delay_millis)) { + onSendFail(packet); + releasePacket(packet); + return false; } + return true; } // Utility function -- handles the case where millis() wraps around back to zero diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 90ee5cdbea..16a1a96487 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -87,7 +87,7 @@ class PacketManager { virtual Packet* allocNew() = 0; virtual void free(Packet* packet) = 0; - virtual void queueOutbound(Packet* packet, uint8_t priority, uint32_t scheduled_for) = 0; + virtual bool queueOutbound(Packet* packet, uint8_t priority, uint32_t scheduled_for) = 0; virtual Packet* getNextOutbound(uint32_t now) = 0; // by priority virtual int getOutboundCount(uint32_t now) const = 0; virtual int getOutboundTotal() const = 0; @@ -171,6 +171,7 @@ class Dispatcher { virtual int getAGCResetInterval() const { return 0; } // disabled by default virtual unsigned long getDutyCycleWindowMs() const { return 3600000; } const Packet* getOutboundInFlight() const { return outbound; } + bool queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis); public: void begin(); @@ -178,7 +179,7 @@ class Dispatcher { Packet* obtainNewPacket(); void releasePacket(Packet* packet); - void sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0); + bool sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0); unsigned long getTotalAirTime() const { return total_air_time; } unsigned long getReceiveAirTime() const {return rx_air_time; } diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 6c9c8080ee..254523bc65 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -5,6 +5,7 @@ namespace mesh { static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 15; static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; +static const uint8_t FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT = 3; static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) { uint8_t code = flags & 0x03; @@ -41,6 +42,18 @@ void Mesh::begin() { _direct_retries[i].queued = false; _direct_retries[i].active = false; } + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + _flood_retries[i].packet = NULL; + _flood_retries[i].trigger_packet = NULL; + _flood_retries[i].retry_started_at = 0; + _flood_retries[i].retry_at = 0; + _flood_retries[i].retry_delay = 0; + _flood_retries[i].retry_attempts_sent = 0; + _flood_retries[i].priority = 0; + _flood_retries[i].progress_marker = 0; + _flood_retries[i].queued = false; + _flood_retries[i].active = false; + } Dispatcher::begin(); } @@ -59,6 +72,19 @@ void Mesh::loop() { clearDirectRetrySlot(i); } } + + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + if (!_flood_retries[i].active || !_flood_retries[i].queued || !millisHasNowPassed(_flood_retries[i].retry_at)) { + continue; + } + + if (!isFloodRetryQueued(_flood_retries[i].packet)) { + if (_flood_retries[i].packet == getOutboundInFlight()) { + continue; + } + clearFloodRetrySlot(i); + } + } } bool Mesh::allowPacketForward(const mesh::Packet* packet) { @@ -87,16 +113,39 @@ uint32_t Mesh::getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_ // Keep the historical linear spacing while allowing the base wait to vary by platform/profile. return base + ((uint32_t)attempt_idx * 100UL); } +bool Mesh::allowFloodRetry(const Packet* packet) const { + return true; +} +bool Mesh::hasFloodRetryTargetPrefix(const Packet* packet) const { + return false; +} +uint8_t Mesh::getFloodRetryMaxPathLength(const Packet* packet) const { + return 2; +} +uint8_t Mesh::getFloodRetryMaxAttempts(const Packet* packet) const { + return FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT; +} +uint32_t Mesh::getFloodRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) { + if (packet == NULL) { + return _radio->getEstAirtimeFor(MAX_TRANS_UNIT); + } + + uint32_t max_packet_airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT); + uint32_t packet_airtime = _radio->getEstAirtimeFor(packet->getRawLength()); + return max_packet_airtime + (20UL * packet_airtime); +} uint8_t Mesh::getExtraAckTransmitCount() const { return 0; } void Mesh::onSendComplete(Packet* packet) { armDirectRetryOnSendComplete(packet); + armFloodRetryOnSendComplete(packet); } void Mesh::onSendFail(Packet* packet) { clearPendingDirectRetryOnSendFail(packet); + clearPendingFloodRetryOnSendFail(packet); } uint32_t Mesh::getCADFailRetryDelay() const { @@ -114,6 +163,8 @@ int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int DispatcherAction Mesh::onRecvPacket(Packet* pkt) { if (pkt->isRouteDirect()) { cancelDirectRetryOnEcho(pkt); + } else if (pkt->isRouteFlood()) { + cancelFloodRetryOnEcho(pkt); } if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { @@ -414,8 +465,10 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) { packet->setPathHashCount(n + 1); uint32_t d = getRetransmitDelay(packet); + uint8_t priority = packet->getPathHashCount(); + maybeScheduleFloodRetry(packet, priority); // as this propagates outwards, give it lower and lower priority - return ACTION_RETRANSMIT_DELAYED(packet->getPathHashCount(), d); // give priority to closer sources, than ones further away + return ACTION_RETRANSMIT_DELAYED(priority, d); // give priority to closer sources, than ones further away } return ACTION_RELEASE; } @@ -589,8 +642,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { *retry = *packet; uint32_t retry_delay = getDirectRetryAttemptDelay(packet, _direct_retries[i].retry_attempts_sent); - sendPacket(retry, _direct_retries[i].priority, retry_delay); - if (isDirectRetryQueued(retry)) { + if (queueOutboundPacket(retry, _direct_retries[i].priority, retry_delay)) { _direct_retries[i].packet = retry; _direct_retries[i].retry_delay = retry_delay; _direct_retries[i].retry_at = futureMillis(retry_delay); @@ -598,6 +650,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { } else { onDirectRetryEvent("dropped_queue_full", retry, retry_delay, _direct_retries[i].retry_attempts_sent + 1); onDirectRetryEvent("failure", retry, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1); + releasePacket(retry); clearDirectRetrySlot(i); } } @@ -620,8 +673,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { *retry = *packet; // Start the echo wait only after the initial direct transmission actually completed. - sendPacket(retry, _direct_retries[i].priority, _direct_retries[i].retry_delay); - if (isDirectRetryQueued(retry)) { + if (queueOutboundPacket(retry, _direct_retries[i].priority, _direct_retries[i].retry_delay)) { unsigned long now = _ms->getMillis(); _direct_retries[i].packet = retry; _direct_retries[i].trigger_packet = NULL; @@ -633,6 +685,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { } else { onDirectRetryEvent("dropped_queue_full", retry, _direct_retries[i].retry_delay, 1); onDirectRetryEvent("failure", retry, 0, 1); + releasePacket(retry); clearDirectRetrySlot(i); } } @@ -759,6 +812,223 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { _direct_retries[slot_idx].active = true; } +void Mesh::clearFloodRetrySlot(int idx) { + _flood_retries[idx].packet = NULL; + _flood_retries[idx].trigger_packet = NULL; + _flood_retries[idx].retry_started_at = 0; + _flood_retries[idx].retry_at = 0; + _flood_retries[idx].retry_delay = 0; + _flood_retries[idx].retry_attempts_sent = 0; + _flood_retries[idx].priority = 0; + _flood_retries[idx].progress_marker = 0; + _flood_retries[idx].queued = false; + _flood_retries[idx].active = false; +} + +bool Mesh::isFloodRetryQueued(const Packet* packet) const { + for (int i = 0; i < _mgr->getOutboundTotal(); i++) { + if (_mgr->getOutboundByIdx(i) == packet) { + return true; + } + } + return false; +} + +bool Mesh::isFloodRetryEchoTarget(const Packet* packet, uint8_t progress_marker) const { + return packet->isRouteFlood() && packet->getPathHashCount() > progress_marker; +} + +bool Mesh::cancelFloodRetryOnEcho(const Packet* packet) { + uint8_t recv_key[MAX_HASH_SIZE]; + packet->calculatePacketHash(recv_key); + + bool cleared = false; + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + if (!_flood_retries[i].active || memcmp(recv_key, _flood_retries[i].retry_key, MAX_HASH_SIZE) != 0) { + continue; + } + if (!isFloodRetryEchoTarget(packet, _flood_retries[i].progress_marker)) { + continue; + } + + uint32_t echo_millis = _flood_retries[i].retry_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at); + const Packet* event_packet = _flood_retries[i].queued ? _flood_retries[i].packet : _flood_retries[i].trigger_packet; + onFloodRetryEvent("good", event_packet, echo_millis, _flood_retries[i].retry_attempts_sent + 1); + + if (_flood_retries[i].queued) { + for (int j = 0; j < _mgr->getOutboundTotal(); j++) { + if (_mgr->getOutboundByIdx(j) == _flood_retries[i].packet) { + Packet* pending = _mgr->removeOutboundByIdx(j); + if (pending) { + releasePacket(pending); + } + break; + } + } + } + clearFloodRetrySlot(i); + cleared = true; + } + + return cleared; +} + +void Mesh::armFloodRetryOnSendComplete(const Packet* packet) { + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + if (!_flood_retries[i].active) { + continue; + } + + if (_flood_retries[i].queued) { + if (_flood_retries[i].packet != packet) { + continue; + } + + uint32_t elapsed_millis = _flood_retries[i].retry_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at); + onFloodRetryEvent("resent", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); + _flood_retries[i].retry_attempts_sent++; + + uint8_t max_attempts = getFloodRetryMaxAttempts(packet); + if (max_attempts < 1) { + max_attempts = 1; + } else if (max_attempts > FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT) { + max_attempts = FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT; + } + if (_flood_retries[i].retry_attempts_sent >= max_attempts) { + onFloodRetryEvent("failed_all_tries", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); + onFloodRetryEvent("failure", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); + clearFloodRetrySlot(i); + continue; + } + + Packet* retry = obtainNewPacket(); + if (retry == NULL) { + onFloodRetryEvent("dropped_no_packet", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); + onFloodRetryEvent("failure", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); + clearFloodRetrySlot(i); + continue; + } + + *retry = *packet; + uint32_t retry_delay = getFloodRetryAttemptDelay(packet, _flood_retries[i].retry_attempts_sent); + if (queueOutboundPacket(retry, _flood_retries[i].priority, retry_delay)) { + _flood_retries[i].packet = retry; + _flood_retries[i].retry_delay = retry_delay; + _flood_retries[i].retry_at = futureMillis(retry_delay); + _flood_retries[i].retry_started_at = _ms->getMillis(); + onFloodRetryEvent("queued", retry, retry_delay, _flood_retries[i].retry_attempts_sent + 1); + } else { + onFloodRetryEvent("dropped_queue_full", retry, retry_delay, _flood_retries[i].retry_attempts_sent + 1); + onFloodRetryEvent("failure", retry, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); + releasePacket(retry); + clearFloodRetrySlot(i); + } + continue; + } + + if (_flood_retries[i].trigger_packet != packet) { + continue; + } + + Packet* retry = obtainNewPacket(); + if (retry == NULL) { + onFloodRetryEvent("dropped_no_packet", packet, _flood_retries[i].retry_delay, 1); + onFloodRetryEvent("failure", packet, 0, 1); + clearFloodRetrySlot(i); + continue; + } + + *retry = *packet; + if (queueOutboundPacket(retry, _flood_retries[i].priority, _flood_retries[i].retry_delay)) { + unsigned long now = _ms->getMillis(); + _flood_retries[i].packet = retry; + _flood_retries[i].trigger_packet = NULL; + _flood_retries[i].queued = true; + _flood_retries[i].retry_at = futureMillis(_flood_retries[i].retry_delay); + _flood_retries[i].retry_started_at = now; + onFloodRetryEvent("queued", retry, _flood_retries[i].retry_delay, 1); + } else { + onFloodRetryEvent("dropped_queue_full", retry, _flood_retries[i].retry_delay, 1); + onFloodRetryEvent("failure", retry, 0, 1); + releasePacket(retry); + clearFloodRetrySlot(i); + } + } +} + +void Mesh::clearPendingFloodRetryOnSendFail(const Packet* packet) { + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + if (!_flood_retries[i].active) { + continue; + } + + if (_flood_retries[i].queued) { + if (_flood_retries[i].packet == packet) { + onFloodRetryEvent("dropped_send_fail", packet, 0, _flood_retries[i].retry_attempts_sent + 1); + onFloodRetryEvent("failure", packet, 0, _flood_retries[i].retry_attempts_sent + 1); + clearFloodRetrySlot(i); + } + continue; + } + + if (_flood_retries[i].trigger_packet == packet) { + onFloodRetryEvent("dropped_send_fail", packet, 0, 1); + onFloodRetryEvent("failure", packet, 0, 1); + clearFloodRetrySlot(i); + } + } +} + +void Mesh::maybeScheduleFloodRetry(const Packet* packet, uint8_t priority) { + if (packet == NULL || !packet->isRouteFlood() || hasFloodRetryTargetPrefix(packet)) { + return; + } + + uint8_t max_path_len = getFloodRetryMaxPathLength(packet); + if (max_path_len != FLOOD_RETRY_PATH_GATE_DISABLED && packet->getPathHashCount() > max_path_len) { + return; + } + + uint8_t max_attempts = getFloodRetryMaxAttempts(packet); + if (max_attempts == 0) { + return; + } + + int slot_idx = -1; + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + if (!_flood_retries[i].active) { + slot_idx = i; + break; + } + } + if (slot_idx < 0) { + onFloodRetryEvent("dropped_no_slot", packet, 0, 0); + onFloodRetryEvent("failure", packet, 0, 0); + return; + } + + if (!allowFloodRetry(packet)) { + return; + } + + uint32_t retry_delay = getFloodRetryAttemptDelay(packet, 0); + packet->calculatePacketHash(_flood_retries[slot_idx].retry_key); + _flood_retries[slot_idx].packet = NULL; + _flood_retries[slot_idx].trigger_packet = const_cast(packet); + _flood_retries[slot_idx].retry_started_at = 0; + _flood_retries[slot_idx].retry_at = 0; + _flood_retries[slot_idx].retry_delay = retry_delay; + _flood_retries[slot_idx].retry_attempts_sent = 0; + _flood_retries[slot_idx].priority = priority; + _flood_retries[slot_idx].progress_marker = packet->getPathHashCount(); + _flood_retries[slot_idx].queued = false; + _flood_retries[slot_idx].active = true; +} + Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) { if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL; diff --git a/src/Mesh.h b/src/Mesh.h index 422b79aba2..69f0519549 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -8,6 +8,14 @@ namespace mesh { #define MAX_DIRECT_RETRY_SLOTS 6 #endif +#ifndef MAX_FLOOD_RETRY_SLOTS + #define MAX_FLOOD_RETRY_SLOTS 6 +#endif + +#ifndef FLOOD_RETRY_PATH_GATE_DISABLED + #define FLOOD_RETRY_PATH_GATE_DISABLED 0xFF +#endif + class GroupChannel { public: uint8_t hash[PATH_HASH_SIZE]; @@ -45,10 +53,25 @@ class Mesh : public Dispatcher { bool active; }; + struct FloodRetryEntry { + Packet* packet; + Packet* trigger_packet; + unsigned long retry_started_at; + unsigned long retry_at; + uint32_t retry_delay; + uint8_t retry_attempts_sent; + uint8_t retry_key[MAX_HASH_SIZE]; + uint8_t priority; + uint8_t progress_marker; + bool queued; + bool active; + }; + RTCClock* _rtc; RNG* _rng; MeshTables* _tables; DirectRetryEntry _direct_retries[MAX_DIRECT_RETRY_SLOTS]; + FloodRetryEntry _flood_retries[MAX_FLOOD_RETRY_SLOTS]; void removeSelfFromPath(Packet* packet); void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis); @@ -61,6 +84,12 @@ class Mesh : public Dispatcher { bool getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len, uint8_t& progress_marker, bool& expect_path_growth) const; void maybeScheduleDirectRetry(const Packet* packet, uint8_t priority); + void clearFloodRetrySlot(int idx); + bool isFloodRetryQueued(const Packet* packet) const; + bool cancelFloodRetryOnEcho(const Packet* packet); + void armFloodRetryOnSendComplete(const Packet* packet); + void clearPendingFloodRetryOnSendFail(const Packet* packet); + void maybeScheduleFloodRetry(const Packet* packet, uint8_t priority); //void routeRecvAcks(Packet* packet, uint32_t delay_millis); DispatcherAction forwardMultipartDirect(Packet* pkt); @@ -119,6 +148,41 @@ class Mesh : public Dispatcher { */ virtual uint32_t getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx); + /** + * \brief Decide whether a FLOOD packet should retry when no downstream echo is overheard. + */ + virtual bool allowFloodRetry(const Packet* packet) const; + + /** + * \brief Return true when this FLOOD packet already carries an application-defined target prefix. + */ + virtual bool hasFloodRetryTargetPrefix(const Packet* packet) const; + + /** + * \returns maximum flood path hash count eligible for retry, or FLOOD_RETRY_PATH_GATE_DISABLED. + */ + virtual uint8_t getFloodRetryMaxPathLength(const Packet* packet) const; + + /** + * \returns maximum number of FLOOD retry transmissions after the initial TX. + */ + virtual uint8_t getFloodRetryMaxAttempts(const Packet* packet) const; + + /** + * \brief Return true when a received FLOOD echo is enough to cancel a pending retry. + */ + virtual bool isFloodRetryEchoTarget(const Packet* packet, uint8_t progress_marker) const; + + /** + * \returns delay before a specific flood retry attempt, where attempt_idx=0 is the first retry. + */ + virtual uint32_t getFloodRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx); + + /** + * \brief Optional hook for logging flood-retry lifecycle events. + */ + virtual void onFloodRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { } + /** * \returns number of extra (Direct) ACK transmissions wanted. */ diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index dc206516f1..27b2eeb460 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -30,6 +30,10 @@ #define DIRECT_RETRY_STEP_MS_MIN 0 #define DIRECT_RETRY_STEP_MS_MAX 5000 #define DIRECT_RETRY_PRESET_DEFAULT DIRECT_RETRY_PRESET_ROOFTOP +#define FLOOD_RETRY_PREFS_MAGIC_0 0xF4 +#define FLOOD_RETRY_PREFS_MAGIC_1 0x52 +#define FLOOD_RETRY_COUNT_MIN 0 +#define FLOOD_RETRY_COUNT_MAX 3 // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { @@ -41,6 +45,30 @@ static uint32_t _atoi(const char* sp) { return n; } +static bool parseUint8Strict(const char* value, uint8_t min_value, uint8_t max_value, uint8_t& result) { + if (value == NULL || *value == 0) { + return false; + } + + uint16_t parsed = 0; + const char* sp = value; + while (*sp) { + if (*sp < '0' || *sp > '9') { + return false; + } + parsed = (uint16_t)((parsed * 10) + (*sp - '0')); + if (parsed > max_value) { + return false; + } + sp++; + } + if (parsed < min_value) { + return false; + } + result = (uint8_t)parsed; + return true; +} + static uint8_t directRetryMarginDbToX4(float margin_db) { int32_t scaled_x4 = (int32_t)((margin_db * 4.0f) + 0.5f); // nearest 0.25 dB return (uint8_t)constrain(scaled_x4, 0, DIRECT_RETRY_SNR_MARGIN_X4_MAX); @@ -97,6 +125,49 @@ static uint16_t directRetryPresetStepDefault(uint8_t preset) { } } +static uint8_t floodRetryPresetCountDefault(uint8_t preset) { + switch (directRetryPresetOrDefault(preset)) { + case DIRECT_RETRY_PRESET_INFRA: + return 1; + case DIRECT_RETRY_PRESET_MOBILE: + case DIRECT_RETRY_PRESET_ROOFTOP: + default: + return 3; + } +} + +static uint8_t floodRetryPresetPathDefault(uint8_t preset) { + switch (directRetryPresetOrDefault(preset)) { + case DIRECT_RETRY_PRESET_INFRA: + return 1; + case DIRECT_RETRY_PRESET_MOBILE: + return 1; + case DIRECT_RETRY_PRESET_ROOFTOP: + default: + return 2; + } +} + +static void applyFloodRetryPreset(NodePrefs* prefs, uint8_t preset) { + prefs->flood_retry_attempts = floodRetryPresetCountDefault(preset); + prefs->flood_retry_path_gate = floodRetryPresetPathDefault(preset); +} + +static uint8_t floodRetryEffectiveCount(const NodePrefs* prefs) { + return constrain(prefs->flood_retry_attempts, (uint8_t)FLOOD_RETRY_COUNT_MIN, (uint8_t)FLOOD_RETRY_COUNT_MAX); +} + +static uint8_t floodRetryPresetForPrefs(const NodePrefs* prefs) { + uint8_t count = floodRetryEffectiveCount(prefs); + uint8_t path_gate = prefs->flood_retry_path_gate; + for (uint8_t preset = DIRECT_RETRY_PRESET_INFRA; preset <= DIRECT_RETRY_PRESET_MOBILE; preset++) { + if (count == floodRetryPresetCountDefault(preset) && path_gate == floodRetryPresetPathDefault(preset)) { + return preset; + } + } + return directRetryPresetOrDefault(prefs->direct_retry_preset); +} + static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) { prefs->direct_retry_preset = directRetryPresetOrDefault(preset); switch (prefs->direct_retry_preset) { @@ -120,6 +191,138 @@ static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) { prefs->direct_retry_snr_margin_db = DIRECT_RETRY_ROOFTOP_MARGIN_X4; break; } + applyFloodRetryPreset(prefs, prefs->direct_retry_preset); +} + +static bool parseFloodRetryPathGate(const char* value, uint8_t& path_gate) { + if (value == NULL) { + return false; + } + if (strcmp(value, "off") == 0 || strcmp(value, "disabled") == 0 || strcmp(value, "disable") == 0) { + path_gate = FLOOD_RETRY_PATH_GATE_DISABLED; + return true; + } + return parseUint8Strict(value, 0, 63, path_gate); +} + +static void formatFloodRetryPathGate(char* dest, uint8_t path_gate) { + if (path_gate == FLOOD_RETRY_PATH_GATE_DISABLED) { + strcpy(dest, "off"); + } else { + sprintf(dest, "%u", (unsigned int)path_gate); + } +} + +static void formatFloodRetryPrefixes(char* dest, const NodePrefs* prefs) { + char* out = dest; + bool first = true; + for (int i = 0; i < FLOOD_RETRY_PREFIX_SLOTS; i++) { + const uint8_t* prefix = prefs->flood_retry_prefixes[i]; + if (prefix[0] == 0 && prefix[1] == 0 && prefix[2] == 0) { + continue; + } + if (!first) { + *out++ = ','; + } + mesh::Utils::toHex(out, prefix, FLOOD_RETRY_PREFIX_LEN); + out += FLOOD_RETRY_PREFIX_LEN * 2; + first = false; + } + *out = 0; +} + +static void formatFloodRetryBridgeBucket(char* dest, const NodePrefs* prefs, uint8_t bucket) { + char* out = dest; + bool first = true; + if (bucket >= FLOOD_RETRY_BRIDGE_BUCKETS) { + *out = 0; + return; + } + for (int i = 0; i < FLOOD_RETRY_BUCKET_PREFIXES; i++) { + const uint8_t* prefix = prefs->flood_retry_bridge_buckets[bucket][i]; + if (prefix[0] == 0 && prefix[1] == 0 && prefix[2] == 0) { + continue; + } + if (!first) { + *out++ = ','; + } + mesh::Utils::toHex(out, prefix, FLOOD_RETRY_PREFIX_LEN); + out += FLOOD_RETRY_PREFIX_LEN * 2; + first = false; + } + *out = 0; +} + +static bool parseFloodRetryPrefixes(NodePrefs* prefs, const char* value) { + uint8_t parsed[FLOOD_RETRY_PREFIX_SLOTS][FLOOD_RETRY_PREFIX_LEN]; + memset(parsed, 0, sizeof(parsed)); + if (value == NULL || value[0] == 0 || strcmp(value, "none") == 0 || strcmp(value, "off") == 0) { + memcpy(prefs->flood_retry_prefixes, parsed, sizeof(prefs->flood_retry_prefixes)); + return true; + } + + char tmp[96]; + StrHelper::strncpy(tmp, value, sizeof(tmp)); + const char* parts[FLOOD_RETRY_PREFIX_SLOTS + 1]; + int num = mesh::Utils::parseTextParts(tmp, parts, FLOOD_RETRY_PREFIX_SLOTS + 1); + if (num > FLOOD_RETRY_PREFIX_SLOTS) { + return false; + } + for (int i = 0; i < num; i++) { + if (strlen(parts[i]) != FLOOD_RETRY_PREFIX_LEN * 2) { + return false; + } + for (int j = 0; j < FLOOD_RETRY_PREFIX_LEN * 2; j++) { + if (!mesh::Utils::isHexChar(parts[i][j])) { + return false; + } + } + if (!mesh::Utils::fromHex(parsed[i], FLOOD_RETRY_PREFIX_LEN, parts[i])) { + return false; + } + const uint8_t* prefix = parsed[i]; + if (prefix[0] == 0 && prefix[1] == 0 && prefix[2] == 0) { + return false; + } + } + memcpy(prefs->flood_retry_prefixes, parsed, sizeof(prefs->flood_retry_prefixes)); + return true; +} + +static bool parseFloodRetryPrefixList(uint8_t dest[][FLOOD_RETRY_PREFIX_LEN], uint8_t max_prefixes, const char* value) { + if (max_prefixes > FLOOD_RETRY_BUCKET_PREFIXES) { + return false; + } + uint8_t parsed[FLOOD_RETRY_BUCKET_PREFIXES][FLOOD_RETRY_PREFIX_LEN]; + memset(parsed, 0, sizeof(parsed)); + if (value == NULL || value[0] == 0 || strcmp(value, "none") == 0 || strcmp(value, "off") == 0) { + memcpy(dest, parsed, max_prefixes * FLOOD_RETRY_PREFIX_LEN); + return true; + } + + char tmp[96]; + StrHelper::strncpy(tmp, value, sizeof(tmp)); + const char* parts[FLOOD_RETRY_BUCKET_PREFIXES + 1]; + int num = mesh::Utils::parseTextParts(tmp, parts, FLOOD_RETRY_BUCKET_PREFIXES + 1); + if (num > max_prefixes) { + return false; + } + for (int i = 0; i < num; i++) { + if (strlen(parts[i]) != FLOOD_RETRY_PREFIX_LEN * 2) { + return false; + } + for (int j = 0; j < FLOOD_RETRY_PREFIX_LEN * 2; j++) { + if (!mesh::Utils::isHexChar(parts[i][j])) { + return false; + } + } + if (!mesh::Utils::fromHex(parsed[i], FLOOD_RETRY_PREFIX_LEN, parts[i]) + || (parsed[i][0] == 0 && parsed[i][1] == 0 && parsed[i][2] == 0)) { + return false; + } + } + memcpy(dest, parsed, max_prefixes * FLOOD_RETRY_PREFIX_LEN); + return true; } static bool parseDirectRetryPreset(const char* value, uint8_t& preset) { @@ -227,6 +430,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->direct_retry_preset, sizeof(_prefs->direct_retry_preset)); // 297 size_t retry_step_read = file.read((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 298 + size_t flood_retry_attempts_read = file.read((uint8_t *)&_prefs->flood_retry_attempts, + sizeof(_prefs->flood_retry_attempts)); // 300 + file.read((uint8_t *)&_prefs->flood_retry_path_gate, sizeof(_prefs->flood_retry_path_gate)); // 301 + file.read((uint8_t *)&_prefs->flood_retry_prefs_magic[0], sizeof(_prefs->flood_retry_prefs_magic)); // 302 + file.read((uint8_t *)&_prefs->flood_retry_prefixes[0][0], sizeof(_prefs->flood_retry_prefixes)); // 304 + file.read((uint8_t *)&_prefs->flood_retry_bridge_enabled, sizeof(_prefs->flood_retry_bridge_enabled)); // 328 + file.read((uint8_t *)&_prefs->flood_retry_bridge_buckets[0][0][0], sizeof(_prefs->flood_retry_bridge_buckets)); // 329 // PowerSaving-only prefs stored radio_fem_rxgain at 291, before direct retry timing existed. if (radio_fem_rxgain_read != sizeof(_prefs->radio_fem_rxgain) && legacy_retry_attempts_read == sizeof(legacy_retry_attempts_or_radio_fem_rxgain) @@ -234,7 +444,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1)) { _prefs->radio_fem_rxgain = constrain(legacy_retry_attempts_or_radio_fem_rxgain, 0, 1); } - // next: 298 + // next: 473 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -288,6 +498,20 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { } else { _prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); } + if (flood_retry_attempts_read != sizeof(_prefs->flood_retry_attempts) + || _prefs->flood_retry_prefs_magic[0] != FLOOD_RETRY_PREFS_MAGIC_0 + || _prefs->flood_retry_prefs_magic[1] != FLOOD_RETRY_PREFS_MAGIC_1) { + applyFloodRetryPreset(_prefs, _prefs->direct_retry_preset); + memset(_prefs->flood_retry_prefixes, 0, sizeof(_prefs->flood_retry_prefixes)); + _prefs->flood_retry_bridge_enabled = 0; + memset(_prefs->flood_retry_bridge_buckets, 0, sizeof(_prefs->flood_retry_bridge_buckets)); + } else { + _prefs->flood_retry_attempts = constrain(_prefs->flood_retry_attempts, FLOOD_RETRY_COUNT_MIN, FLOOD_RETRY_COUNT_MAX); + if (_prefs->flood_retry_path_gate > 63 && _prefs->flood_retry_path_gate != FLOOD_RETRY_PATH_GATE_DISABLED) { + _prefs->flood_retry_path_gate = floodRetryPresetPathDefault(_prefs->direct_retry_preset); + } + _prefs->flood_retry_bridge_enabled = constrain(_prefs->flood_retry_bridge_enabled, 0, 1); + } file.close(); } @@ -360,7 +584,14 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 296 file.write((uint8_t *)&_prefs->direct_retry_preset, sizeof(_prefs->direct_retry_preset)); // 297 file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 298 - // next: 300 + file.write((uint8_t *)&_prefs->flood_retry_attempts, sizeof(_prefs->flood_retry_attempts)); // 300 + file.write((uint8_t *)&_prefs->flood_retry_path_gate, sizeof(_prefs->flood_retry_path_gate)); // 301 + uint8_t flood_retry_magic[2] = { FLOOD_RETRY_PREFS_MAGIC_0, FLOOD_RETRY_PREFS_MAGIC_1 }; + file.write(flood_retry_magic, sizeof(flood_retry_magic)); // 302 + file.write((uint8_t *)&_prefs->flood_retry_prefixes[0][0], sizeof(_prefs->flood_retry_prefixes)); // 304 + file.write((uint8_t *)&_prefs->flood_retry_bridge_enabled, sizeof(_prefs->flood_retry_bridge_enabled)); // 328 + file.write((uint8_t *)&_prefs->flood_retry_bridge_buckets[0][0][0], sizeof(_prefs->flood_retry_bridge_buckets)); // 329 + // next: 473 file.close(); } @@ -516,6 +747,30 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor)); } else if (memcmp(config, "flood.max", 9) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); + } else if (memcmp(config, "flood.retry.count", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)floodRetryEffectiveCount(_prefs)); + } else if (memcmp(config, "flood.retry.path", 16) == 0) { + char path_gate[8]; + formatFloodRetryPathGate(path_gate, _prefs->flood_retry_path_gate); + sprintf(reply, "> %s", path_gate); + } else if (memcmp(config, "flood.retry.prefixes", 20) == 0) { + formatFloodRetryPrefixes(tmp, _prefs); + sprintf(reply, "> %s", tmp[0] ? tmp : "none"); + } else if (memcmp(config, "flood.retry.bridge", 18) == 0) { + sprintf(reply, "> %s", _prefs->flood_retry_bridge_enabled ? "on" : "off"); + } else if (memcmp(config, "flood.retry.bucket.", 19) == 0) { + uint8_t bucket = atoi(&config[19]); + if (bucket >= 1 && bucket <= FLOOD_RETRY_BRIDGE_BUCKETS) { + formatFloodRetryBridgeBucket(tmp, _prefs, bucket - 1); + sprintf(reply, "> %s", tmp[0] ? tmp : "none"); + } else { + sprintf(reply, "Error, bucket 1-%d", FLOOD_RETRY_BRIDGE_BUCKETS); + } + } else if (memcmp(config, "flood.retry.preset", 18) == 0) { + uint8_t preset = floodRetryPresetForPrefs(_prefs); + sprintf(reply, "> %d,%s", + (uint32_t)preset, + directRetryPresetName(preset)); } else if (memcmp(config, "direct.txdelay", 14) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); } else if (memcmp(config, "direct.retry.heard", 18) == 0) { @@ -772,6 +1027,65 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { strcpy(reply, "Error, max 64"); } + } else if (memcmp(config, "flood.retry.preset ", 19) == 0) { + uint8_t preset; + if (parseDirectRetryPreset(&config[19], preset)) { + applyFloodRetryPreset(_prefs, preset); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be infra, rooftop, mobile, 0, 1, or 2"); + } + } else if (memcmp(config, "flood.retry.count ", 18) == 0) { + uint8_t count; + if (parseUint8Strict(&config[18], FLOOD_RETRY_COUNT_MIN, FLOOD_RETRY_COUNT_MAX, count)) { + _prefs->flood_retry_attempts = count; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", FLOOD_RETRY_COUNT_MIN, FLOOD_RETRY_COUNT_MAX); + } + } else if (memcmp(config, "flood.retry.path ", 17) == 0) { + uint8_t path_gate; + if (parseFloodRetryPathGate(&config[17], path_gate)) { + _prefs->flood_retry_path_gate = path_gate; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-63 or off"); + } + } else if (memcmp(config, "flood.retry.prefixes ", 21) == 0) { + if (parseFloodRetryPrefixes(_prefs, &config[21])) { + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, use comma-separated 3-byte hex prefixes"); + } + } else if (memcmp(config, "flood.retry.bridge ", 19) == 0) { + if (memcmp(&config[19], "on", 2) == 0) { + _prefs->flood_retry_bridge_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(&config[19], "off", 3) == 0) { + _prefs->flood_retry_bridge_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be on or off"); + } + } else if (memcmp(config, "flood.retry.bucket ", 19) == 0) { + const char* params = &config[19]; + uint8_t bucket = atoi(params); + const char* list = strchr(params, ' '); + if (bucket < 1 || bucket > FLOOD_RETRY_BRIDGE_BUCKETS || list == NULL || *(list + 1) == 0) { + sprintf(reply, "Error, usage: set flood.retry.bucket <1-%d> ", FLOOD_RETRY_BRIDGE_BUCKETS); + } else if (parseFloodRetryPrefixList(_prefs->flood_retry_bridge_buckets[bucket - 1], + FLOOD_RETRY_BUCKET_PREFIXES, list + 1)) { + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, use up to 8 comma-separated 3-byte hex prefixes"); + } } else if (memcmp(config, "direct.txdelay ", 15) == 0) { float f = atof(&config[15]); if (f >= 0) { @@ -1326,6 +1640,65 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, max 64"); } + } else if (memcmp(config, "flood.retry.preset ", 19) == 0) { + uint8_t preset; + if (parseDirectRetryPreset(&config[19], preset)) { + applyFloodRetryPreset(_prefs, preset); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be infra, rooftop, mobile, 0, 1, or 2"); + } + } else if (memcmp(config, "flood.retry.count ", 18) == 0) { + uint8_t count; + if (parseUint8Strict(&config[18], FLOOD_RETRY_COUNT_MIN, FLOOD_RETRY_COUNT_MAX, count)) { + _prefs->flood_retry_attempts = count; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", FLOOD_RETRY_COUNT_MIN, FLOOD_RETRY_COUNT_MAX); + } + } else if (memcmp(config, "flood.retry.path ", 17) == 0) { + uint8_t path_gate; + if (parseFloodRetryPathGate(&config[17], path_gate)) { + _prefs->flood_retry_path_gate = path_gate; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-63 or off"); + } + } else if (memcmp(config, "flood.retry.prefixes ", 21) == 0) { + if (parseFloodRetryPrefixes(_prefs, &config[21])) { + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, use comma-separated 3-byte hex prefixes"); + } + } else if (memcmp(config, "flood.retry.bridge ", 19) == 0) { + if (memcmp(&config[19], "on", 2) == 0) { + _prefs->flood_retry_bridge_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(&config[19], "off", 3) == 0) { + _prefs->flood_retry_bridge_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be on or off"); + } + } else if (memcmp(config, "flood.retry.bucket ", 19) == 0) { + const char* params = &config[19]; + uint8_t bucket = atoi(params); + const char* list = strchr(params, ' '); + if (bucket < 1 || bucket > FLOOD_RETRY_BRIDGE_BUCKETS || list == NULL || *(list + 1) == 0) { + sprintf(reply, "Error, usage: set flood.retry.bucket <1-%d> ", FLOOD_RETRY_BRIDGE_BUCKETS); + } else if (parseFloodRetryPrefixList(_prefs->flood_retry_bridge_buckets[bucket - 1], + FLOOD_RETRY_BUCKET_PREFIXES, list + 1)) { + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, use up to 8 comma-separated 3-byte hex prefixes"); + } } else if (memcmp(config, "direct.txdelay ", 15) == 0) { float f = atof(&config[15]); if (f >= 0) { @@ -1565,6 +1938,30 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor)); } else if (memcmp(config, "flood.max", 9) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); + } else if (memcmp(config, "flood.retry.count", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)floodRetryEffectiveCount(_prefs)); + } else if (memcmp(config, "flood.retry.path", 16) == 0) { + char path_gate[8]; + formatFloodRetryPathGate(path_gate, _prefs->flood_retry_path_gate); + sprintf(reply, "> %s", path_gate); + } else if (memcmp(config, "flood.retry.prefixes", 20) == 0) { + formatFloodRetryPrefixes(tmp, _prefs); + sprintf(reply, "> %s", tmp[0] ? tmp : "none"); + } else if (memcmp(config, "flood.retry.bridge", 18) == 0) { + sprintf(reply, "> %s", _prefs->flood_retry_bridge_enabled ? "on" : "off"); + } else if (memcmp(config, "flood.retry.bucket.", 19) == 0) { + uint8_t bucket = atoi(&config[19]); + if (bucket >= 1 && bucket <= FLOOD_RETRY_BRIDGE_BUCKETS) { + formatFloodRetryBridgeBucket(tmp, _prefs, bucket - 1); + sprintf(reply, "> %s", tmp[0] ? tmp : "none"); + } else { + sprintf(reply, "Error, bucket 1-%d", FLOOD_RETRY_BRIDGE_BUCKETS); + } + } else if (memcmp(config, "flood.retry.preset", 18) == 0) { + uint8_t preset = floodRetryPresetForPrefs(_prefs); + sprintf(reply, "> %d,%s", + (uint32_t)preset, + directRetryPresetName(preset)); } else if (memcmp(config, "direct.txdelay", 14) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); } else if (memcmp(config, "direct.retry.heard", 18) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index ea30777a2d..321c3c8fa0 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -38,6 +38,19 @@ #define DIRECT_RETRY_MOBILE_STEP_MS 50 #define DIRECT_RETRY_MOBILE_MARGIN_X4 0 +#ifndef FLOOD_RETRY_PREFIX_SLOTS + #define FLOOD_RETRY_PREFIX_SLOTS 8 +#endif +#ifndef FLOOD_RETRY_PREFIX_LEN + #define FLOOD_RETRY_PREFIX_LEN 3 +#endif +#ifndef FLOOD_RETRY_BRIDGE_BUCKETS + #define FLOOD_RETRY_BRIDGE_BUCKETS 6 +#endif +#ifndef FLOOD_RETRY_BUCKET_PREFIXES + #define FLOOD_RETRY_BUCKET_PREFIXES 8 +#endif + struct NodePrefs { // persisted to file float airtime_factor; char node_name[32]; @@ -88,6 +101,12 @@ struct NodePrefs { // persisted to file uint8_t direct_retry_timing_magic[2]; uint8_t direct_retry_preset; uint16_t direct_retry_step_ms; + uint8_t flood_retry_attempts; + uint8_t flood_retry_path_gate; + uint8_t flood_retry_prefs_magic[2]; + uint8_t flood_retry_prefixes[FLOOD_RETRY_PREFIX_SLOTS][FLOOD_RETRY_PREFIX_LEN]; + uint8_t flood_retry_bridge_enabled; + uint8_t flood_retry_bridge_buckets[FLOOD_RETRY_BRIDGE_BUCKETS][FLOOD_RETRY_BUCKET_PREFIXES][FLOOD_RETRY_PREFIX_LEN]; }; class CommonCLICallbacks { diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index ae28acc8fd..ca81b144ff 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -1,6 +1,9 @@ #pragma once #include +#if ARDUINO + #include +#endif #ifdef ESP32 #include @@ -30,6 +33,7 @@ class SimpleMeshTables : public mesh::MeshTables { uint8_t prefix_len; int8_t snr_x4; uint8_t snr_locked; + uint32_t last_heard_millis; }; private: @@ -178,6 +182,8 @@ class SimpleMeshTables : public mesh::MeshTables { bool hasSeen(const mesh::Packet* packet) override { if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { + recordRecentRepeater(packet); + uint32_t ack; memcpy(&ack, packet->payload, 4); @@ -295,6 +301,11 @@ class SimpleMeshTables : public mesh::MeshTables { } else if (!existing.snr_locked) { existing.snr_x4 = weightedSnrX4RoundUp(existing.snr_x4, snr_x4); } +#if ARDUINO + existing.last_heard_millis = millis(); +#else + existing.last_heard_millis = 0; +#endif return true; } @@ -325,6 +336,11 @@ class SimpleMeshTables : public mesh::MeshTables { slot.prefix_len = prefix_len; slot.snr_x4 = snr_x4; slot.snr_locked = snr_locked ? 1 : 0; +#if ARDUINO + slot.last_heard_millis = millis(); +#else + slot.last_heard_millis = 0; +#endif _next_recent_repeater_idx = (slot_idx + 1) % MAX_RECENT_REPEATERS; return true; } diff --git a/src/helpers/StaticPoolPacketManager.cpp b/src/helpers/StaticPoolPacketManager.cpp index b8926df0cc..fc2bb059fa 100644 --- a/src/helpers/StaticPoolPacketManager.cpp +++ b/src/helpers/StaticPoolPacketManager.cpp @@ -83,11 +83,12 @@ void StaticPoolPacketManager::free(mesh::Packet* packet) { unused.add(packet, 0, 0); } -void StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { +bool StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { if (!send_queue.add(packet, priority, scheduled_for)) { MESH_DEBUG_PRINTLN("queueOutbound: send queue full, dropping packet"); - free(packet); + return false; } + return true; } mesh::Packet* StaticPoolPacketManager::getNextOutbound(uint32_t now) { diff --git a/src/helpers/StaticPoolPacketManager.h b/src/helpers/StaticPoolPacketManager.h index 59715b4e01..350e85d29d 100644 --- a/src/helpers/StaticPoolPacketManager.h +++ b/src/helpers/StaticPoolPacketManager.h @@ -26,7 +26,7 @@ class StaticPoolPacketManager : public mesh::PacketManager { mesh::Packet* allocNew() override; void free(mesh::Packet* packet) override; - void queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) override; + bool queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) override; mesh::Packet* getNextOutbound(uint32_t now) override; int getOutboundCount(uint32_t now) const override; int getOutboundTotal() const override; @@ -35,4 +35,4 @@ class StaticPoolPacketManager : public mesh::PacketManager { mesh::Packet* removeOutboundByIdx(int i) override; void queueInbound(mesh::Packet* packet, uint32_t scheduled_for) override; mesh::Packet* getNextInbound(uint32_t now) override; -}; \ No newline at end of file +}; From 93bd0f086947b951b206fcfec5f0c45c2616fc0d Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 7 May 2026 16:40:31 -0700 Subject: [PATCH 30/94] Fix flood retry diagnostics --- docs/cli_commands.md | 4 +- examples/simple_repeater/MyMesh.cpp | 253 ++++++++++++++++++++++------ examples/simple_repeater/MyMesh.h | 3 + src/Mesh.cpp | 50 +++++- src/Mesh.h | 1 + 5 files changed, 255 insertions(+), 56 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 79d319adfe..c1041042a3 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -132,8 +132,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore - Rows are sorted by prefix width (3-byte, 2-byte, 1-byte), then SNR descending. - A full direct retry failure lowers the stored SNR by `0.25 dB`. - If a full failure has no row yet, it first seeds the row at the active retry cutoff + `2.5 dB`, then applies the `0.25 dB` penalty. -- Serial CLI prints all rows (no paging). -- Over LoRa remote CLI, page size is fixed at `4` rows; choose page with `get recent.repeater `. +- Serial CLI page size is fixed at `128` rows; choose page with `get recent.repeater `. +- Over LoRa remote CLI, page size is fixed at `7` rows; choose page with `get recent.repeater `. --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 404a26d606..ce848ef229 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1,5 +1,6 @@ #include "MyMesh.h" #include +#include /* ------------------------------ Config -------------------------------- */ @@ -1068,11 +1069,148 @@ void MyMesh::clearFloodRetryBridgeState(const mesh::Packet* packet) { state->active = false; } } +void MyMesh::refreshFloodRetryHeardRecent(const mesh::Packet* packet) { + if (packet == NULL || !packet->isRouteFlood() || packet->getPathHashCount() == 0) { + return; + } + + uint8_t hash_size = packet->getPathHashSize(); + if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { + return; + } + + auto* tables = (SimpleMeshTables*)getTables(); + const uint8_t* path = packet->path; + if (_prefs.flood_retry_bridge_enabled) { + FloodRetryBridgeState* state = floodRetryBridgeStateFor(packet, false); + if (state != NULL) { + for (int hop = 0; hop < packet->getPathHashCount(); hop++) { + int bucket = floodRetryBucketForPrefix(path, hash_size, true); + if (bucket >= 0 && bucket != state->source_bucket && (state->target_mask & (uint8_t)(1U << bucket))) { + tables->setRecentRepeater(path, hash_size, packet->_snr, false, true); + } + path += hash_size; + } + return; + } + } + + const uint8_t* heard_prefix = &packet->path[(packet->getPathHashCount() - 1) * hash_size]; + tables->setRecentRepeater(heard_prefix, hash_size, packet->_snr, false, true); +} +void MyMesh::formatFloodRetryPath(char* dest, size_t dest_len, const mesh::Packet* packet) const { + if (dest == NULL || dest_len == 0) { + return; + } + dest[0] = 0; + + if (packet == NULL || packet->getPathHashCount() == 0) { + StrHelper::strncpy(dest, "-", dest_len); + return; + } + + uint8_t hash_size = packet->getPathHashSize(); + if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { + StrHelper::strncpy(dest, "invalid", dest_len); + return; + } + + char* out = dest; + size_t remaining = dest_len; + const uint8_t* path = packet->path; + for (int hop = 0; hop < packet->getPathHashCount(); hop++) { + size_t needed = (hop > 0 ? 1 : 0) + ((size_t)hash_size * 2) + 1; + if (remaining < needed) { + if (remaining > 4) { + strcpy(out, "..."); + } + return; + } + if (hop > 0) { + *out++ = '>'; + remaining--; + } + mesh::Utils::toHex(out, path, hash_size); + out += (size_t)hash_size * 2; + remaining -= (size_t)hash_size * 2; + path += hash_size; + } +} +bool MyMesh::formatFloodRetryHeard(char* dest, size_t dest_len, const mesh::Packet* packet) const { + if (dest == NULL || dest_len == 0 || packet == NULL || packet->getPathHashCount() == 0) { + return false; + } + dest[0] = 0; + + uint8_t hash_size = packet->getPathHashSize(); + if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { + return false; + } + + char* out = dest; + size_t remaining = dest_len; + bool first = true; + + if (_prefs.flood_retry_bridge_enabled) { + FloodRetryBridgeState* state = floodRetryBridgeStateFor(packet, false); + if (state == NULL) { + return false; + } + const uint8_t* path = packet->path; + for (int hop = 0; hop < packet->getPathHashCount(); hop++) { + int bucket = floodRetryBucketForPrefix(path, hash_size, true); + if (bucket >= 0 && bucket != state->source_bucket && (state->target_mask & (uint8_t)(1U << bucket))) { + size_t needed = (first ? 0 : 1) + 2 + 1 + ((size_t)hash_size * 2) + 1; + if (remaining < needed) { + if (remaining > 4) { + strcpy(out, "..."); + } + return dest[0] != 0; + } + if (!first) { + *out++ = ','; + remaining--; + } + int n = snprintf(out, remaining, "b%d:", bucket + 1); + if (n < 0 || (size_t)n >= remaining) { + return dest[0] != 0; + } + out += n; + remaining -= n; + mesh::Utils::toHex(out, path, hash_size); + out += (size_t)hash_size * 2; + remaining -= (size_t)hash_size * 2; + first = false; + } + path += hash_size; + } + return dest[0] != 0; + } + + const uint8_t* heard_prefix = &packet->path[(packet->getPathHashCount() - 1) * hash_size]; + if (remaining < ((size_t)hash_size * 2) + 1) { + return false; + } + mesh::Utils::toHex(out, heard_prefix, hash_size); + return true; +} void MyMesh::onFloodRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { if (event == NULL || packet == NULL) { return; } + bool clear_bridge_state = _prefs.flood_retry_bridge_enabled + && (strcmp(event, "good") == 0 || strcmp(event, "failure") == 0 || strcmp(event, "failed_all_tries") == 0 + || strncmp(event, "dropped_", 8) == 0); + + if (clear_bridge_state && strcmp(event, "failure") == 0) { + clearFloodRetryBridgeState(packet); + } + + if (strcmp(event, "failure") == 0) { + return; + } + const char* time_label = "time_ms"; if (strcmp(event, "queued") == 0 || strcmp(event, "dropped_queue_full") == 0) { time_label = "wait_ms"; @@ -1083,7 +1221,17 @@ void MyMesh::onFloodRetryEvent(const char* event, const mesh::Packet* packet, ui time_label = "echo_ms"; } - MESH_DEBUG_PRINTLN("%s flood retry %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%u, %s=%lu)", + char path_log[208]; + char heard_log[96]; + char heard_suffix[112]; + formatFloodRetryPath(path_log, sizeof(path_log), packet); + heard_suffix[0] = 0; + if (strcmp(event, "good") == 0 && formatFloodRetryHeard(heard_log, sizeof(heard_log), packet)) { + refreshFloodRetryHeardRecent(packet); + snprintf(heard_suffix, sizeof(heard_suffix), ", heard=%s", heard_log); + } + + MESH_DEBUG_PRINTLN("%s flood retry %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%u, path=%s%s, %s=%lu)", getLogDateTime(), event, (unsigned int)retry_attempt, @@ -1091,6 +1239,8 @@ void MyMesh::onFloodRetryEvent(const char* event, const mesh::Packet* packet, ui packet->isRouteDirect() ? "D" : "F", (uint32_t)packet->payload_len, (unsigned int)packet->getPathHashCount(), + path_log, + heard_suffix, time_label, (unsigned long)delay_millis); @@ -1098,22 +1248,22 @@ void MyMesh::onFloodRetryEvent(const char* event, const mesh::Packet* packet, ui File f = openAppend(PACKET_LOG_FILE); if (f) { f.print(getLogDateTime()); - f.printf(": FLOOD RETRY %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%u, %s=%lu)\n", + f.printf(": FLOOD RETRY %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%u, path=%s%s, %s=%lu)\n", event, (unsigned int)retry_attempt, (uint32_t)packet->getPayloadType(), packet->isRouteDirect() ? "D" : "F", (uint32_t)packet->payload_len, (unsigned int)packet->getPathHashCount(), + path_log, + heard_suffix, time_label, (unsigned long)delay_millis); f.close(); } } - if (_prefs.flood_retry_bridge_enabled - && (strcmp(event, "failure") == 0 || strcmp(event, "failed_all_tries") == 0 - || strncmp(event, "dropped_", 8) == 0)) { + if (clear_bridge_state) { clearFloodRetryBridgeState(packet); } } @@ -1146,11 +1296,7 @@ bool MyMesh::isFloodRetryEchoTarget(const mesh::Packet* packet, uint8_t progress return false; } state->heard_mask |= floodRetryBridgeHeardMask(packet, state->source_bucket) & state->target_mask; - bool complete = (state->heard_mask & state->target_mask) == state->target_mask; - if (complete) { - state->active = false; - } - return complete; + return (state->heard_mask & state->target_mask) == state->target_mask; } if (hasFloodRetryPrefixes()) { return floodRetryLastHopMatches(packet); @@ -1979,16 +2125,58 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } } else { - const SimpleMeshTables::RecentRepeaterInfo* sorted_recent[MAX_RECENT_REPEATERS]; + const long page_size = sender_timestamp == 0 ? 128 : 7; + long page_num = 1; + const char* arg = sub; + + if (strncmp(arg, "page ", 5) == 0) { + arg += 5; + while (*arg == ' ') arg++; + } + + if (*arg != 0) { + char* end_ptr = NULL; + page_num = strtol(arg, &end_ptr, 10); + while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; + if (end_ptr == NULL || page_num <= 0 || (end_ptr != NULL && *end_ptr != 0)) { + strcpy(reply, "Err - usage: get recent.repeater [page]"); + return; + } + } + + size_t sorted_size = sizeof(SimpleMeshTables::RecentRepeaterInfo*) * MAX_RECENT_REPEATERS; + const SimpleMeshTables::RecentRepeaterInfo** sorted_recent = + (const SimpleMeshTables::RecentRepeaterInfo**)malloc(sorted_size); + if (sorted_recent == NULL) { + strcpy(reply, "Err - unable to allocate recent repeater view"); + return; + } + int total = buildSortedRecentRepeaterView(tables, sorted_recent, MAX_RECENT_REPEATERS); if (total <= 0) { strcpy(reply, "> none"); } else { + int total_pages = (total + (int)page_size - 1) / (int)page_size; + if (page_num > total_pages) { + sprintf(reply, "> none (page=%ld/%d)", page_num, total_pages); + free(sorted_recent); + return; + } + + int offset = ((int)page_num - 1) * (int)page_size; + int limit = total - offset; + if (limit > (int)page_size) { + limit = (int)page_size; + } + if (sender_timestamp == 0) { - // Serial CLI: print all entries (no paging). - Serial.printf("Recent repeater table (3-byte,2-byte,1-byte; SNR desc, total=%d):\n", total); - for (int i = 0; i < total; i++) { - const auto* info = sorted_recent[i]; + Serial.printf("Recent repeater table (3-byte,2-byte,1-byte; SNR desc, page=%ld/%d, n=%d/%d):\n", + page_num, + total_pages, + limit, + total); + for (int i = 0; i < limit; i++) { + const auto* info = sorted_recent[offset + i]; if (info == NULL) { continue; } @@ -2002,40 +2190,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply snr_text, info->snr_locked ? ",l" : ""); } - sprintf(reply, "> n=%d/%d", total, total); + sprintf(reply, "> page=%ld/%d n=%d/%d", page_num, total_pages, limit, total); } else { - // Remote CLI: page by fixed size to fit packet-limited reply payload. - long page_num = 1; - const long page_size = 4; - const char* arg = sub; - - if (strncmp(arg, "page ", 5) == 0) { - arg += 5; - while (*arg == ' ') arg++; - } - - if (*arg != 0) { - char* end_ptr = NULL; - page_num = strtol(arg, &end_ptr, 10); - while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; - if (end_ptr == NULL || page_num <= 0 || (end_ptr != NULL && *end_ptr != 0)) { - strcpy(reply, "Err - usage: get recent.repeater [page]"); - return; - } - } - - int total_pages = (total + (int)page_size - 1) / (int)page_size; - if (page_num > total_pages) { - sprintf(reply, "> none (page=%ld/%d)", page_num, total_pages); - return; - } - - int offset = ((int)page_num - 1) * (int)page_size; - int limit = total - offset; - if (limit > (int)page_size) { - limit = (int)page_size; - } - int written = snprintf(reply, 160, "> page=%ld/%d n=%d/%d", page_num, total_pages, limit, total); bool truncated = false; if (written < 0) { @@ -2075,6 +2231,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } } + free(sorted_recent); } } else if (memcmp(command, "discover.neighbors", 18) == 0) { const char* sub = command + 18; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 80c41b029c..c189bda971 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -145,6 +145,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t source_bucket) const; FloodRetryBridgeState* floodRetryBridgeStateFor(const mesh::Packet* packet, bool create) const; void clearFloodRetryBridgeState(const mesh::Packet* packet); + void refreshFloodRetryHeardRecent(const mesh::Packet* packet); + void formatFloodRetryPath(char* dest, size_t dest_len, const mesh::Packet* packet) const; + bool formatFloodRetryHeard(char* dest, size_t dest_len, const mesh::Packet* packet) const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 254523bc65..4671be4459 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -51,6 +51,7 @@ void Mesh::begin() { _flood_retries[i].retry_attempts_sent = 0; _flood_retries[i].priority = 0; _flood_retries[i].progress_marker = 0; + _flood_retries[i].waiting_final_echo = false; _flood_retries[i].queued = false; _flood_retries[i].active = false; } @@ -74,7 +75,25 @@ void Mesh::loop() { } for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { - if (!_flood_retries[i].active || !_flood_retries[i].queued || !millisHasNowPassed(_flood_retries[i].retry_at)) { + if (!_flood_retries[i].active) { + continue; + } + + if (_flood_retries[i].waiting_final_echo) { + if (!millisHasNowPassed(_flood_retries[i].retry_at)) { + continue; + } + + uint32_t elapsed_millis = _flood_retries[i].retry_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at); + onFloodRetryEvent("failed_all_tries", _flood_retries[i].packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); + onFloodRetryEvent("failure", _flood_retries[i].packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); + clearFloodRetrySlot(i); + continue; + } + + if (!_flood_retries[i].queued || !millisHasNowPassed(_flood_retries[i].retry_at)) { continue; } @@ -813,6 +832,9 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { } void Mesh::clearFloodRetrySlot(int idx) { + if (_flood_retries[idx].waiting_final_echo && _flood_retries[idx].packet != NULL) { + releasePacket(_flood_retries[idx].packet); + } _flood_retries[idx].packet = NULL; _flood_retries[idx].trigger_packet = NULL; _flood_retries[idx].retry_started_at = 0; @@ -821,6 +843,7 @@ void Mesh::clearFloodRetrySlot(int idx) { _flood_retries[idx].retry_attempts_sent = 0; _flood_retries[idx].priority = 0; _flood_retries[idx].progress_marker = 0; + _flood_retries[idx].waiting_final_echo = false; _flood_retries[idx].queued = false; _flood_retries[idx].active = false; } @@ -854,8 +877,10 @@ bool Mesh::cancelFloodRetryOnEcho(const Packet* packet) { uint32_t echo_millis = _flood_retries[i].retry_started_at == 0 ? 0 : (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at); - const Packet* event_packet = _flood_retries[i].queued ? _flood_retries[i].packet : _flood_retries[i].trigger_packet; - onFloodRetryEvent("good", event_packet, echo_millis, _flood_retries[i].retry_attempts_sent + 1); + uint8_t retry_attempt = _flood_retries[i].waiting_final_echo + ? _flood_retries[i].retry_attempts_sent + : _flood_retries[i].retry_attempts_sent + 1; + onFloodRetryEvent("good", packet, echo_millis, retry_attempt); if (_flood_retries[i].queued) { for (int j = 0; j < _mgr->getOutboundTotal(); j++) { @@ -899,9 +924,19 @@ void Mesh::armFloodRetryOnSendComplete(const Packet* packet) { max_attempts = FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT; } if (_flood_retries[i].retry_attempts_sent >= max_attempts) { - onFloodRetryEvent("failed_all_tries", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); - onFloodRetryEvent("failure", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); - clearFloodRetrySlot(i); + Packet* final_wait = obtainNewPacket(); + if (final_wait == NULL) { + onFloodRetryEvent("dropped_no_packet", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); + onFloodRetryEvent("failure", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); + clearFloodRetrySlot(i); + continue; + } + + *final_wait = *packet; + _flood_retries[i].packet = final_wait; + _flood_retries[i].retry_at = futureMillis(_flood_retries[i].retry_delay); + _flood_retries[i].waiting_final_echo = true; + _flood_retries[i].queued = false; continue; } @@ -920,6 +955,7 @@ void Mesh::armFloodRetryOnSendComplete(const Packet* packet) { _flood_retries[i].retry_delay = retry_delay; _flood_retries[i].retry_at = futureMillis(retry_delay); _flood_retries[i].retry_started_at = _ms->getMillis(); + _flood_retries[i].waiting_final_echo = false; onFloodRetryEvent("queued", retry, retry_delay, _flood_retries[i].retry_attempts_sent + 1); } else { onFloodRetryEvent("dropped_queue_full", retry, retry_delay, _flood_retries[i].retry_attempts_sent + 1); @@ -948,6 +984,7 @@ void Mesh::armFloodRetryOnSendComplete(const Packet* packet) { _flood_retries[i].packet = retry; _flood_retries[i].trigger_packet = NULL; _flood_retries[i].queued = true; + _flood_retries[i].waiting_final_echo = false; _flood_retries[i].retry_at = futureMillis(_flood_retries[i].retry_delay); _flood_retries[i].retry_started_at = now; onFloodRetryEvent("queued", retry, _flood_retries[i].retry_delay, 1); @@ -1025,6 +1062,7 @@ void Mesh::maybeScheduleFloodRetry(const Packet* packet, uint8_t priority) { _flood_retries[slot_idx].retry_attempts_sent = 0; _flood_retries[slot_idx].priority = priority; _flood_retries[slot_idx].progress_marker = packet->getPathHashCount(); + _flood_retries[slot_idx].waiting_final_echo = false; _flood_retries[slot_idx].queued = false; _flood_retries[slot_idx].active = true; } diff --git a/src/Mesh.h b/src/Mesh.h index 69f0519549..33adf91823 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -63,6 +63,7 @@ class Mesh : public Dispatcher { uint8_t retry_key[MAX_HASH_SIZE]; uint8_t priority; uint8_t progress_marker; + bool waiting_final_echo; bool queued; bool active; }; From e0b19a1f60b0be43a1cbc33a639568aad1a6a483 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 7 May 2026 16:51:49 -0700 Subject: [PATCH 31/94] Fix direct retry diagnostics --- docs/cli_commands.md | 4 +- examples/simple_repeater/MyMesh.cpp | 91 +++++++++++++++++------------ src/Mesh.cpp | 65 +++++++++++++++++---- src/Mesh.h | 1 + 4 files changed, 109 insertions(+), 52 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index f9c0825976..bf77872f84 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -132,8 +132,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore - Rows are sorted by prefix width (3-byte, 2-byte, 1-byte), then SNR descending. - A full direct retry failure lowers the stored SNR by `0.25 dB`. - If a full failure has no row yet, it first seeds the row at the active retry cutoff + `2.5 dB`, then applies the `0.25 dB` penalty. -- Serial CLI prints all rows (no paging). -- Over LoRa remote CLI, page size is fixed at `4` rows; choose page with `get recent.repeater `. +- Serial CLI page size is fixed at `128` rows; choose page with `get recent.repeater `. +- Over LoRa remote CLI, page size is fixed at `7` rows; choose page with `get recent.repeater `. --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 6b3e8d6a7d..f9a961b9f4 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1,5 +1,6 @@ #include "MyMesh.h" #include +#include /* ------------------------------ Config -------------------------------- */ @@ -661,6 +662,9 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u if (packet == NULL) { return; } + if (strcmp(event, "failure") == 0) { + return; + } uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; uint8_t prefix_len = 0; @@ -1686,16 +1690,58 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } } else { - const SimpleMeshTables::RecentRepeaterInfo* sorted_recent[MAX_RECENT_REPEATERS]; + const long page_size = sender_timestamp == 0 ? 128 : 7; + long page_num = 1; + const char* arg = sub; + + if (strncmp(arg, "page ", 5) == 0) { + arg += 5; + while (*arg == ' ') arg++; + } + + if (*arg != 0) { + char* end_ptr = NULL; + page_num = strtol(arg, &end_ptr, 10); + while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; + if (end_ptr == NULL || page_num <= 0 || (end_ptr != NULL && *end_ptr != 0)) { + strcpy(reply, "Err - usage: get recent.repeater [page]"); + return; + } + } + + size_t sorted_size = sizeof(SimpleMeshTables::RecentRepeaterInfo*) * MAX_RECENT_REPEATERS; + const SimpleMeshTables::RecentRepeaterInfo** sorted_recent = + (const SimpleMeshTables::RecentRepeaterInfo**)malloc(sorted_size); + if (sorted_recent == NULL) { + strcpy(reply, "Err - unable to allocate recent repeater view"); + return; + } + int total = buildSortedRecentRepeaterView(tables, sorted_recent, MAX_RECENT_REPEATERS); if (total <= 0) { strcpy(reply, "> none"); } else { + int total_pages = (total + (int)page_size - 1) / (int)page_size; + if (page_num > total_pages) { + sprintf(reply, "> none (page=%ld/%d)", page_num, total_pages); + free(sorted_recent); + return; + } + + int offset = ((int)page_num - 1) * (int)page_size; + int limit = total - offset; + if (limit > (int)page_size) { + limit = (int)page_size; + } + if (sender_timestamp == 0) { - // Serial CLI: print all entries (no paging). - Serial.printf("Recent repeater table (3-byte,2-byte,1-byte; SNR desc, total=%d):\n", total); - for (int i = 0; i < total; i++) { - const auto* info = sorted_recent[i]; + Serial.printf("Recent repeater table (3-byte,2-byte,1-byte; SNR desc, page=%ld/%d, n=%d/%d):\n", + page_num, + total_pages, + limit, + total); + for (int i = 0; i < limit; i++) { + const auto* info = sorted_recent[offset + i]; if (info == NULL) { continue; } @@ -1709,40 +1755,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply snr_text, info->snr_locked ? ",l" : ""); } - sprintf(reply, "> n=%d/%d", total, total); + sprintf(reply, "> page=%ld/%d n=%d/%d", page_num, total_pages, limit, total); } else { - // Remote CLI: page by fixed size to fit packet-limited reply payload. - long page_num = 1; - const long page_size = 4; - const char* arg = sub; - - if (strncmp(arg, "page ", 5) == 0) { - arg += 5; - while (*arg == ' ') arg++; - } - - if (*arg != 0) { - char* end_ptr = NULL; - page_num = strtol(arg, &end_ptr, 10); - while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; - if (end_ptr == NULL || page_num <= 0 || (end_ptr != NULL && *end_ptr != 0)) { - strcpy(reply, "Err - usage: get recent.repeater [page]"); - return; - } - } - - int total_pages = (total + (int)page_size - 1) / (int)page_size; - if (page_num > total_pages) { - sprintf(reply, "> none (page=%ld/%d)", page_num, total_pages); - return; - } - - int offset = ((int)page_num - 1) * (int)page_size; - int limit = total - offset; - if (limit > (int)page_size) { - limit = (int)page_size; - } - int written = snprintf(reply, 160, "> page=%ld/%d n=%d/%d", page_num, total_pages, limit, total); bool truncated = false; if (written < 0) { @@ -1782,6 +1796,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } } + free(sorted_recent); } } else if (memcmp(command, "discover.neighbors", 18) == 0) { const char* sub = command + 18; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 6c9c8080ee..42bcf5b10e 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -38,6 +38,7 @@ void Mesh::begin() { _direct_retries[i].priority = 0; _direct_retries[i].progress_marker = 0; _direct_retries[i].expect_path_growth = false; + _direct_retries[i].waiting_final_echo = false; _direct_retries[i].queued = false; _direct_retries[i].active = false; } @@ -48,7 +49,25 @@ void Mesh::loop() { Dispatcher::loop(); for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { - if (!_direct_retries[i].active || !_direct_retries[i].queued || !millisHasNowPassed(_direct_retries[i].retry_at)) { + if (!_direct_retries[i].active) { + continue; + } + + if (_direct_retries[i].waiting_final_echo) { + if (!millisHasNowPassed(_direct_retries[i].retry_at)) { + continue; + } + + uint32_t elapsed_millis = _direct_retries[i].retry_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at); + onDirectRetryEvent("failed_all_tries", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + onDirectRetryEvent("failure", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + clearDirectRetrySlot(i); + continue; + } + + if (!_direct_retries[i].queued || !millisHasNowPassed(_direct_retries[i].retry_at)) { continue; } @@ -470,6 +489,9 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { } void Mesh::clearDirectRetrySlot(int idx) { + if (_direct_retries[idx].waiting_final_echo && _direct_retries[idx].packet != NULL) { + releasePacket(_direct_retries[idx].packet); + } _direct_retries[idx].packet = NULL; _direct_retries[idx].trigger_packet = NULL; _direct_retries[idx].retry_started_at = 0; @@ -480,6 +502,7 @@ void Mesh::clearDirectRetrySlot(int idx) { _direct_retries[idx].priority = 0; _direct_retries[idx].progress_marker = 0; _direct_retries[idx].expect_path_growth = false; + _direct_retries[idx].waiting_final_echo = false; _direct_retries[idx].queued = false; _direct_retries[idx].active = false; } @@ -516,7 +539,7 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { } int8_t echo_snr_x4 = packet->_snr; - if (_direct_retries[i].queued) { + if (_direct_retries[i].queued || _direct_retries[i].waiting_final_echo) { if (_direct_retries[i].packet != NULL) { // Success quality comes from the received downstream echo, not the original upstream RX. _direct_retries[i].packet->_snr = echo_snr_x4; @@ -524,14 +547,19 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { uint32_t echo_millis = _direct_retries[i].echo_wait_started_at == 0 ? 0 : (uint32_t)(_ms->getMillis() - _direct_retries[i].echo_wait_started_at); - onDirectRetryEvent("good", _direct_retries[i].packet, echo_millis, _direct_retries[i].retry_attempts_sent + 1); - for (int j = 0; j < _mgr->getOutboundTotal(); j++) { - if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { - Packet* pending = _mgr->removeOutboundByIdx(j); - if (pending) { - releasePacket(pending); + uint8_t retry_attempt = _direct_retries[i].waiting_final_echo + ? _direct_retries[i].retry_attempts_sent + : _direct_retries[i].retry_attempts_sent + 1; + onDirectRetryEvent("good", _direct_retries[i].packet, echo_millis, retry_attempt); + if (_direct_retries[i].queued) { + for (int j = 0; j < _mgr->getOutboundTotal(); j++) { + if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { + Packet* pending = _mgr->removeOutboundByIdx(j); + if (pending) { + releasePacket(pending); + } + break; } - break; } } clearDirectRetrySlot(i); @@ -573,9 +601,19 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX; } if (_direct_retries[i].retry_attempts_sent >= max_attempts) { - onDirectRetryEvent("failed_all_tries", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); - onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); - clearDirectRetrySlot(i); + Packet* final_wait = obtainNewPacket(); + if (final_wait == NULL) { + onDirectRetryEvent("dropped_no_packet", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + clearDirectRetrySlot(i); + continue; + } + + *final_wait = *packet; + _direct_retries[i].packet = final_wait; + _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); + _direct_retries[i].waiting_final_echo = true; + _direct_retries[i].queued = false; continue; } @@ -594,6 +632,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { _direct_retries[i].packet = retry; _direct_retries[i].retry_delay = retry_delay; _direct_retries[i].retry_at = futureMillis(retry_delay); + _direct_retries[i].waiting_final_echo = false; onDirectRetryEvent("queued", retry, retry_delay, _direct_retries[i].retry_attempts_sent + 1); } else { onDirectRetryEvent("dropped_queue_full", retry, retry_delay, _direct_retries[i].retry_attempts_sent + 1); @@ -626,6 +665,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { _direct_retries[i].packet = retry; _direct_retries[i].trigger_packet = NULL; _direct_retries[i].queued = true; + _direct_retries[i].waiting_final_echo = false; _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); _direct_retries[i].retry_started_at = now; _direct_retries[i].echo_wait_started_at = now; @@ -755,6 +795,7 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { _direct_retries[slot_idx].priority = priority; _direct_retries[slot_idx].progress_marker = progress_marker; _direct_retries[slot_idx].expect_path_growth = expect_path_growth; + _direct_retries[slot_idx].waiting_final_echo = false; _direct_retries[slot_idx].queued = false; _direct_retries[slot_idx].active = true; } diff --git a/src/Mesh.h b/src/Mesh.h index 422b79aba2..91aec9a669 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -41,6 +41,7 @@ class Mesh : public Dispatcher { uint8_t priority; uint8_t progress_marker; bool expect_path_growth; + bool waiting_final_echo; bool queued; bool active; }; From 8dcd4975d5d4e3b6aa05f7d5bc6c67b5b0b85d3d Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 7 May 2026 16:55:37 -0700 Subject: [PATCH 32/94] Delay direct retry final failure --- examples/simple_repeater/MyMesh.cpp | 3 ++ src/Mesh.cpp | 65 +++++++++++++++++++++++------ src/Mesh.h | 1 + 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index ce848ef229..ca86d27e69 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -662,6 +662,9 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u if (packet == NULL) { return; } + if (strcmp(event, "failure") == 0) { + return; + } uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; uint8_t prefix_len = 0; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 4671be4459..109b1384c7 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -39,6 +39,7 @@ void Mesh::begin() { _direct_retries[i].priority = 0; _direct_retries[i].progress_marker = 0; _direct_retries[i].expect_path_growth = false; + _direct_retries[i].waiting_final_echo = false; _direct_retries[i].queued = false; _direct_retries[i].active = false; } @@ -62,7 +63,25 @@ void Mesh::loop() { Dispatcher::loop(); for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { - if (!_direct_retries[i].active || !_direct_retries[i].queued || !millisHasNowPassed(_direct_retries[i].retry_at)) { + if (!_direct_retries[i].active) { + continue; + } + + if (_direct_retries[i].waiting_final_echo) { + if (!millisHasNowPassed(_direct_retries[i].retry_at)) { + continue; + } + + uint32_t elapsed_millis = _direct_retries[i].retry_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at); + onDirectRetryEvent("failed_all_tries", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + onDirectRetryEvent("failure", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + clearDirectRetrySlot(i); + continue; + } + + if (!_direct_retries[i].queued || !millisHasNowPassed(_direct_retries[i].retry_at)) { continue; } @@ -542,6 +561,9 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { } void Mesh::clearDirectRetrySlot(int idx) { + if (_direct_retries[idx].waiting_final_echo && _direct_retries[idx].packet != NULL) { + releasePacket(_direct_retries[idx].packet); + } _direct_retries[idx].packet = NULL; _direct_retries[idx].trigger_packet = NULL; _direct_retries[idx].retry_started_at = 0; @@ -552,6 +574,7 @@ void Mesh::clearDirectRetrySlot(int idx) { _direct_retries[idx].priority = 0; _direct_retries[idx].progress_marker = 0; _direct_retries[idx].expect_path_growth = false; + _direct_retries[idx].waiting_final_echo = false; _direct_retries[idx].queued = false; _direct_retries[idx].active = false; } @@ -588,7 +611,7 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { } int8_t echo_snr_x4 = packet->_snr; - if (_direct_retries[i].queued) { + if (_direct_retries[i].queued || _direct_retries[i].waiting_final_echo) { if (_direct_retries[i].packet != NULL) { // Success quality comes from the received downstream echo, not the original upstream RX. _direct_retries[i].packet->_snr = echo_snr_x4; @@ -596,14 +619,19 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { uint32_t echo_millis = _direct_retries[i].echo_wait_started_at == 0 ? 0 : (uint32_t)(_ms->getMillis() - _direct_retries[i].echo_wait_started_at); - onDirectRetryEvent("good", _direct_retries[i].packet, echo_millis, _direct_retries[i].retry_attempts_sent + 1); - for (int j = 0; j < _mgr->getOutboundTotal(); j++) { - if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { - Packet* pending = _mgr->removeOutboundByIdx(j); - if (pending) { - releasePacket(pending); + uint8_t retry_attempt = _direct_retries[i].waiting_final_echo + ? _direct_retries[i].retry_attempts_sent + : _direct_retries[i].retry_attempts_sent + 1; + onDirectRetryEvent("good", _direct_retries[i].packet, echo_millis, retry_attempt); + if (_direct_retries[i].queued) { + for (int j = 0; j < _mgr->getOutboundTotal(); j++) { + if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { + Packet* pending = _mgr->removeOutboundByIdx(j); + if (pending) { + releasePacket(pending); + } + break; } - break; } } clearDirectRetrySlot(i); @@ -645,9 +673,19 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX; } if (_direct_retries[i].retry_attempts_sent >= max_attempts) { - onDirectRetryEvent("failed_all_tries", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); - onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); - clearDirectRetrySlot(i); + Packet* final_wait = obtainNewPacket(); + if (final_wait == NULL) { + onDirectRetryEvent("dropped_no_packet", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + clearDirectRetrySlot(i); + continue; + } + + *final_wait = *packet; + _direct_retries[i].packet = final_wait; + _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); + _direct_retries[i].waiting_final_echo = true; + _direct_retries[i].queued = false; continue; } @@ -665,6 +703,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { _direct_retries[i].packet = retry; _direct_retries[i].retry_delay = retry_delay; _direct_retries[i].retry_at = futureMillis(retry_delay); + _direct_retries[i].waiting_final_echo = false; onDirectRetryEvent("queued", retry, retry_delay, _direct_retries[i].retry_attempts_sent + 1); } else { onDirectRetryEvent("dropped_queue_full", retry, retry_delay, _direct_retries[i].retry_attempts_sent + 1); @@ -697,6 +736,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { _direct_retries[i].packet = retry; _direct_retries[i].trigger_packet = NULL; _direct_retries[i].queued = true; + _direct_retries[i].waiting_final_echo = false; _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); _direct_retries[i].retry_started_at = now; _direct_retries[i].echo_wait_started_at = now; @@ -827,6 +867,7 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { _direct_retries[slot_idx].priority = priority; _direct_retries[slot_idx].progress_marker = progress_marker; _direct_retries[slot_idx].expect_path_growth = expect_path_growth; + _direct_retries[slot_idx].waiting_final_echo = false; _direct_retries[slot_idx].queued = false; _direct_retries[slot_idx].active = true; } diff --git a/src/Mesh.h b/src/Mesh.h index 33adf91823..f76334a6b5 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -49,6 +49,7 @@ class Mesh : public Dispatcher { uint8_t priority; uint8_t progress_marker; bool expect_path_growth; + bool waiting_final_echo; bool queued; bool active; }; From 3a6766b0fddb6f29c105a3f08a1009ed2cdcda12 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 8 May 2026 00:49:33 -0700 Subject: [PATCH 33/94] Improve flood retry bridge buckets --- build-iotthinks.sh | 23 ++++---- docs/cli_commands.md | 18 +++++- examples/simple_repeater/MyMesh.cpp | 92 ++++++++++++++++++++++++----- examples/simple_repeater/MyMesh.h | 10 +++- src/helpers/CommonCLI.cpp | 84 +++++++++++++++++--------- src/helpers/CommonCLI.h | 17 +++++- src/helpers/SimpleMeshTables.h | 2 - 7 files changed, 183 insertions(+), 63 deletions(-) mode change 100644 => 100755 build-iotthinks.sh diff --git a/build-iotthinks.sh b/build-iotthinks.sh old mode 100644 new mode 100755 index 7c654482c7..3e41b4d694 --- a/build-iotthinks.sh +++ b/build-iotthinks.sh @@ -1,10 +1,11 @@ -# sh ./build-repeaters-iotthinks.sh +#!/usr/bin/env bash +# ./build-iotthinks.sh export FIRMWARE_VERSION="PowerSaving15" ############# Repeaters ############# # Commonly-used boards ## ESP32 - 12 boards -sh build.sh build-firmware \ +./build.sh build-firmware \ Heltec_v3_repeater \ Heltec_WSL3_repeater \ heltec_v4_repeater \ @@ -19,7 +20,7 @@ Heltec_E290_repeater \ Heltec_Wireless_Tracker_repeater ## NRF52 - 13 boards -sh build.sh build-firmware \ +./build.sh build-firmware \ RAK_4631_repeater \ Heltec_t114_repeater \ Xiao_nrf52_repeater \ @@ -35,25 +36,25 @@ GAT562_30S_Mesh_Kit_repeater \ GAT562_Mesh_Tracker_Pro_repeater ## ESP32, SX1276 - 3 boards -sh build.sh build-firmware \ +./build.sh build-firmware \ Heltec_v2_repeater \ LilyGo_TLora_V2_1_1_6_repeater \ Tbeam_SX1276_repeater ## Ikoka - 3 boards -sh build.sh build-firmware \ +./build.sh build-firmware \ ikoka_nano_nrf_22dbm_repeater \ ikoka_nano_nrf_30dbm_repeater \ ikoka_nano_nrf_33dbm_repeater ############# Room Server ############# # ESP32 -sh build.sh build-firmware \ +./build.sh build-firmware \ Heltec_v3_room_server \ heltec_v4_room_server # NRF52 -sh build.sh build-firmware \ +./build.sh build-firmware \ RAK_4631_room_server \ Heltec_t114_room_server \ Xiao_nrf52_room_server \ @@ -63,7 +64,7 @@ RAK_3401_room_server ############# Companions BLE ############# # ESP32 -sh build.sh build-firmware \ +./build.sh build-firmware \ Heltec_v3_companion_radio_ble_ps \ heltec_v4_companion_radio_ble_ps \ heltec_v4_companion_radio_ble_ps_femoff \ @@ -71,7 +72,7 @@ Xiao_S3_WIO_companion_radio_ble \ Heltec_Wireless_Paper_companion_radio_ble # NRF52 -sh build.sh build-firmware \ +./build.sh build-firmware \ RAK_4631_companion_radio_ble \ Heltec_t114_companion_radio_ble \ Xiao_nrf52_companion_radio_ble \ @@ -82,11 +83,11 @@ RAK_3401_companion_radio_ble \ RAK_WisMesh_Tag_companion_radio_ble ############# Companions USB ############# -sh build.sh build-firmware \ +./build.sh build-firmware \ Heltec_v3_companion_radio_usb ############# Companions BLE PS ############# -sh build.sh build-firmware \ +./build.sh build-firmware \ Heltec_v3_companion_radio_ble_ps \ heltec_v4_companion_radio_ble_ps \ heltec_v4_3_companion_radio_ble_ps_femoff \ diff --git a/docs/cli_commands.md b/docs/cli_commands.md index c1041042a3..1a6e4233e5 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -821,7 +821,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Default:** `off` -**Note:** Bridge mode uses bucket definitions instead of the single `flood.retry.prefixes` target list. If a flood comes from one fresh bucket, retry continues until every other fresh configured bucket has been heard or `flood.retry.count` is exhausted. +**Note:** Bridge mode uses bucket definitions instead of the single `flood.retry.prefixes` target list. It also has an implicit unconfigured catch-all bucket. If a flood comes from one fresh configured bucket, retry continues until every other fresh configured bucket plus the catch-all bucket has been heard or `flood.retry.count` is exhausted. If a flood comes from an unconfigured or pathless source, retry targets every fresh configured bucket. This means one configured bucket bridges between that bucket and everything else. Prefixes in `flood.retry.ignore` never count as heard bridge targets. --- @@ -832,7 +832,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `bucket`: Bucket number (`1`-`6`) -- `prefixes`: Up to 8 comma-separated 3-byte hex prefixes, such as `AABBCC,223344`; use `none` or `off` to clear +- `prefixes`: Up to 17 comma-separated 3-byte hex prefixes, such as `AABBCC,223344`; use `none` or `off` to clear **Default:** all buckets empty @@ -840,6 +840,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change flood retry ignored prefixes +**Usage:** +- `get flood.retry.ignore` +- `set flood.retry.ignore ` + +**Parameters:** +- `prefixes`: Up to 8 comma-separated 3-byte hex prefixes, such as `AABBCC,223344`; use `none` or `off` to clear + +**Default:** empty + +**Note:** Ignored prefixes do not count as a heard bridge bucket or as the implicit catch-all bucket when bridge retry decides whether every target has repeated the flood. + +--- + ### ACL #### Add, update or remove permissions for a companion diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index ca86d27e69..3fa770e91f 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -912,6 +912,19 @@ bool MyMesh::floodRetryPrefixMatches(const mesh::Packet* packet) const { return false; } +bool MyMesh::floodRetryPrefixIgnored(const uint8_t* prefix, uint8_t prefix_len) const { + if (prefix == NULL || prefix_len == 0 || prefix_len > MAX_ROUTE_HASH_BYTES) { + return false; + } + for (int i = 0; i < FLOOD_RETRY_IGNORE_PREFIXES; i++) { + const uint8_t* ignored = _prefs.flood_retry_ignore_prefixes[i]; + if ((ignored[0] != 0 || ignored[1] != 0 || ignored[2] != 0) + && memcmp(ignored, prefix, prefix_len) == 0) { + return true; + } + } + return false; +} bool MyMesh::floodRetryPrefixFresh(const uint8_t* prefix, uint8_t prefix_len) const { const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(prefix, prefix_len); if (recent == NULL || recent->last_heard_millis == 0) { @@ -919,10 +932,22 @@ bool MyMesh::floodRetryPrefixFresh(const uint8_t* prefix, uint8_t prefix_len) co } return (uint32_t)(millis() - recent->last_heard_millis) <= 3600000UL; } -int MyMesh::floodRetryBucketForPrefix(const uint8_t* prefix, uint8_t prefix_len, bool require_fresh) const { +static const uint8_t FLOOD_RETRY_BRIDGE_OTHER_BUCKET = FLOOD_RETRY_BRIDGE_BUCKETS; + +static uint8_t floodRetryBucketMask(uint8_t bucket) { + if (bucket >= 8) { + return 0; + } + return (uint8_t)(1U << bucket); +} +int MyMesh::floodRetryBucketForPrefix(const uint8_t* prefix, uint8_t prefix_len, bool require_fresh, + bool include_other) const { if (prefix == NULL || prefix_len == 0 || prefix_len > MAX_ROUTE_HASH_BYTES) { return -1; } + if (floodRetryPrefixIgnored(prefix, prefix_len)) { + return -1; + } if (require_fresh && !floodRetryPrefixFresh(prefix, prefix_len)) { return -1; } @@ -935,18 +960,28 @@ int MyMesh::floodRetryBucketForPrefix(const uint8_t* prefix, uint8_t prefix_len, } } } + if (include_other) { + return FLOOD_RETRY_BRIDGE_OTHER_BUCKET; + } return -1; } +int MyMesh::floodRetryBucketForPathHop(const uint8_t* prefix, uint8_t prefix_len, uint8_t hop, + uint8_t progress_marker) const { + return floodRetryBucketForPrefix(prefix, prefix_len, hop < progress_marker, true); +} int MyMesh::floodRetrySourceBucket(const mesh::Packet* packet) const { - if (packet == NULL || packet->getPathHashCount() < 2) { + if (packet == NULL) { return -1; } uint8_t hash_size = packet->getPathHashSize(); if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { return -1; } + if (packet->getPathHashCount() < 2) { + return FLOOD_RETRY_BRIDGE_OTHER_BUCKET; + } const uint8_t* source_prefix = &packet->path[(packet->getPathHashCount() - 2) * hash_size]; - return floodRetryBucketForPrefix(source_prefix, hash_size, true); + return floodRetryBucketForPrefix(source_prefix, hash_size, true, true); } uint8_t MyMesh::floodRetryBridgeTargetMask(uint8_t source_bucket) const { uint8_t mask = 0; @@ -957,15 +992,20 @@ uint8_t MyMesh::floodRetryBridgeTargetMask(uint8_t source_bucket) const { for (int i = 0; i < FLOOD_RETRY_BUCKET_PREFIXES; i++) { const uint8_t* configured = _prefs.flood_retry_bridge_buckets[bucket][i]; if ((configured[0] != 0 || configured[1] != 0 || configured[2] != 0) + && !floodRetryPrefixIgnored(configured, FLOOD_RETRY_PREFIX_LEN) && floodRetryPrefixFresh(configured, FLOOD_RETRY_PREFIX_LEN)) { - mask |= (uint8_t)(1U << bucket); + mask |= floodRetryBucketMask((uint8_t)bucket); break; } } } + if (source_bucket != FLOOD_RETRY_BRIDGE_OTHER_BUCKET) { + mask |= floodRetryBucketMask(FLOOD_RETRY_BRIDGE_OTHER_BUCKET); + } return mask; } -uint8_t MyMesh::floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t source_bucket) const { +uint8_t MyMesh::floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t source_bucket, + uint8_t progress_marker) const { if (packet == NULL || packet->getPathHashCount() == 0) { return 0; } @@ -977,9 +1017,13 @@ uint8_t MyMesh::floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t so uint8_t mask = 0; const uint8_t* path = packet->path; for (int hop = 0; hop < packet->getPathHashCount(); hop++) { - int bucket = floodRetryBucketForPrefix(path, hash_size, true); + if (progress_marker > 0 && hop == progress_marker - 1) { + path += hash_size; + continue; + } + int bucket = floodRetryBucketForPathHop(path, hash_size, (uint8_t)hop, progress_marker); if (bucket >= 0 && bucket != source_bucket) { - mask |= (uint8_t)(1U << bucket); + mask |= floodRetryBucketMask((uint8_t)bucket); } path += hash_size; } @@ -1019,7 +1063,8 @@ MyMesh::FloodRetryBridgeState* MyMesh::floodRetryBridgeStateFor(const mesh::Pack return NULL; } - uint8_t heard_mask = floodRetryBridgeHeardMask(packet, (uint8_t)source_bucket) & target_mask; + uint8_t progress_marker = packet->getPathHashCount(); + uint8_t heard_mask = floodRetryBridgeHeardMask(packet, (uint8_t)source_bucket, progress_marker) & target_mask; if ((heard_mask & target_mask) == target_mask) { return NULL; } @@ -1029,6 +1074,7 @@ MyMesh::FloodRetryBridgeState* MyMesh::floodRetryBridgeStateFor(const mesh::Pack free_slot->source_bucket = (uint8_t)source_bucket; free_slot->target_mask = target_mask; free_slot->heard_mask = heard_mask; + free_slot->progress_marker = progress_marker; free_slot->active = true; return free_slot; } @@ -1088,8 +1134,13 @@ void MyMesh::refreshFloodRetryHeardRecent(const mesh::Packet* packet) { FloodRetryBridgeState* state = floodRetryBridgeStateFor(packet, false); if (state != NULL) { for (int hop = 0; hop < packet->getPathHashCount(); hop++) { - int bucket = floodRetryBucketForPrefix(path, hash_size, true); - if (bucket >= 0 && bucket != state->source_bucket && (state->target_mask & (uint8_t)(1U << bucket))) { + if (state->progress_marker > 0 && hop == state->progress_marker - 1) { + path += hash_size; + continue; + } + int bucket = floodRetryBucketForPathHop(path, hash_size, (uint8_t)hop, state->progress_marker); + uint8_t bucket_mask = bucket >= 0 ? floodRetryBucketMask((uint8_t)bucket) : 0; + if (bucket >= 0 && bucket != state->source_bucket && (state->target_mask & bucket_mask)) { tables->setRecentRepeater(path, hash_size, packet->_snr, false, true); } path += hash_size; @@ -1161,9 +1212,20 @@ bool MyMesh::formatFloodRetryHeard(char* dest, size_t dest_len, const mesh::Pack } const uint8_t* path = packet->path; for (int hop = 0; hop < packet->getPathHashCount(); hop++) { - int bucket = floodRetryBucketForPrefix(path, hash_size, true); - if (bucket >= 0 && bucket != state->source_bucket && (state->target_mask & (uint8_t)(1U << bucket))) { - size_t needed = (first ? 0 : 1) + 2 + 1 + ((size_t)hash_size * 2) + 1; + if (state->progress_marker > 0 && hop == state->progress_marker - 1) { + path += hash_size; + continue; + } + int bucket = floodRetryBucketForPathHop(path, hash_size, (uint8_t)hop, state->progress_marker); + uint8_t bucket_mask = bucket >= 0 ? floodRetryBucketMask((uint8_t)bucket) : 0; + if (bucket >= 0 && bucket != state->source_bucket && (state->target_mask & bucket_mask)) { + char bucket_label[8]; + if ((uint8_t)bucket == FLOOD_RETRY_BRIDGE_OTHER_BUCKET) { + strcpy(bucket_label, "other"); + } else { + snprintf(bucket_label, sizeof(bucket_label), "b%d", bucket + 1); + } + size_t needed = (first ? 0 : 1) + strlen(bucket_label) + 1 + ((size_t)hash_size * 2) + 1; if (remaining < needed) { if (remaining > 4) { strcpy(out, "..."); @@ -1174,7 +1236,7 @@ bool MyMesh::formatFloodRetryHeard(char* dest, size_t dest_len, const mesh::Pack *out++ = ','; remaining--; } - int n = snprintf(out, remaining, "b%d:", bucket + 1); + int n = snprintf(out, remaining, "%s:", bucket_label); if (n < 0 || (size_t)n >= remaining) { return dest[0] != 0; } @@ -1298,7 +1360,7 @@ bool MyMesh::isFloodRetryEchoTarget(const mesh::Packet* packet, uint8_t progress if (state == NULL) { return false; } - state->heard_mask |= floodRetryBridgeHeardMask(packet, state->source_bucket) & state->target_mask; + state->heard_mask |= floodRetryBridgeHeardMask(packet, state->source_bucket, state->progress_marker) & state->target_mask; return (state->heard_mask & state->target_mask) == state->target_mask; } if (hasFloodRetryPrefixes()) { diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index c189bda971..6716acd12f 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -104,6 +104,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t source_bucket; uint8_t target_mask; uint8_t heard_mask; + uint8_t progress_marker; bool active; }; mutable FloodRetryBridgeState flood_retry_bridge_states[MAX_FLOOD_RETRY_SLOTS]; @@ -138,11 +139,16 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { bool hasFloodRetryPrefixes() const; bool floodRetryPrefixMatches(const mesh::Packet* packet) const; bool floodRetryLastHopMatches(const mesh::Packet* packet) const; + bool floodRetryPrefixIgnored(const uint8_t* prefix, uint8_t prefix_len) const; bool floodRetryPrefixFresh(const uint8_t* prefix, uint8_t prefix_len) const; - int floodRetryBucketForPrefix(const uint8_t* prefix, uint8_t prefix_len, bool require_fresh) const; + int floodRetryBucketForPrefix(const uint8_t* prefix, uint8_t prefix_len, bool require_fresh, + bool include_other) const; + int floodRetryBucketForPathHop(const uint8_t* prefix, uint8_t prefix_len, uint8_t hop, + uint8_t progress_marker) const; int floodRetrySourceBucket(const mesh::Packet* packet) const; uint8_t floodRetryBridgeTargetMask(uint8_t source_bucket) const; - uint8_t floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t source_bucket) const; + uint8_t floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t source_bucket, + uint8_t progress_marker) const; FloodRetryBridgeState* floodRetryBridgeStateFor(const mesh::Packet* packet, bool create) const; void clearFloodRetryBridgeState(const mesh::Packet* packet); void refreshFloodRetryHeardRecent(const mesh::Packet* packet); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 27b2eeb460..ab4513ca33 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -213,11 +213,12 @@ static void formatFloodRetryPathGate(char* dest, uint8_t path_gate) { } } -static void formatFloodRetryPrefixes(char* dest, const NodePrefs* prefs) { +static void formatFloodRetryPrefixList(char* dest, const uint8_t prefixes[][FLOOD_RETRY_PREFIX_LEN], + uint8_t max_prefixes) { char* out = dest; bool first = true; - for (int i = 0; i < FLOOD_RETRY_PREFIX_SLOTS; i++) { - const uint8_t* prefix = prefs->flood_retry_prefixes[i]; + for (int i = 0; i < max_prefixes; i++) { + const uint8_t* prefix = prefixes[i]; if (prefix[0] == 0 && prefix[1] == 0 && prefix[2] == 0) { continue; } @@ -231,26 +232,16 @@ static void formatFloodRetryPrefixes(char* dest, const NodePrefs* prefs) { *out = 0; } +static void formatFloodRetryPrefixes(char* dest, const NodePrefs* prefs) { + formatFloodRetryPrefixList(dest, prefs->flood_retry_prefixes, FLOOD_RETRY_PREFIX_SLOTS); +} + static void formatFloodRetryBridgeBucket(char* dest, const NodePrefs* prefs, uint8_t bucket) { - char* out = dest; - bool first = true; if (bucket >= FLOOD_RETRY_BRIDGE_BUCKETS) { - *out = 0; + dest[0] = 0; return; } - for (int i = 0; i < FLOOD_RETRY_BUCKET_PREFIXES; i++) { - const uint8_t* prefix = prefs->flood_retry_bridge_buckets[bucket][i]; - if (prefix[0] == 0 && prefix[1] == 0 && prefix[2] == 0) { - continue; - } - if (!first) { - *out++ = ','; - } - mesh::Utils::toHex(out, prefix, FLOOD_RETRY_PREFIX_LEN); - out += FLOOD_RETRY_PREFIX_LEN * 2; - first = false; - } - *out = 0; + formatFloodRetryPrefixList(dest, prefs->flood_retry_bridge_buckets[bucket], FLOOD_RETRY_BUCKET_PREFIXES); } static bool parseFloodRetryPrefixes(NodePrefs* prefs, const char* value) { @@ -261,7 +252,7 @@ static bool parseFloodRetryPrefixes(NodePrefs* prefs, const char* value) { return true; } - char tmp[96]; + char tmp[FLOOD_RETRY_LIST_TEXT_MAX]; StrHelper::strncpy(tmp, value, sizeof(tmp)); const char* parts[FLOOD_RETRY_PREFIX_SLOTS + 1]; int num = mesh::Utils::parseTextParts(tmp, parts, FLOOD_RETRY_PREFIX_SLOTS + 1); @@ -290,20 +281,20 @@ static bool parseFloodRetryPrefixes(NodePrefs* prefs, const char* value) { } static bool parseFloodRetryPrefixList(uint8_t dest[][FLOOD_RETRY_PREFIX_LEN], uint8_t max_prefixes, const char* value) { - if (max_prefixes > FLOOD_RETRY_BUCKET_PREFIXES) { + if (max_prefixes > FLOOD_RETRY_LIST_PREFIXES) { return false; } - uint8_t parsed[FLOOD_RETRY_BUCKET_PREFIXES][FLOOD_RETRY_PREFIX_LEN]; + uint8_t parsed[FLOOD_RETRY_LIST_PREFIXES][FLOOD_RETRY_PREFIX_LEN]; memset(parsed, 0, sizeof(parsed)); if (value == NULL || value[0] == 0 || strcmp(value, "none") == 0 || strcmp(value, "off") == 0) { memcpy(dest, parsed, max_prefixes * FLOOD_RETRY_PREFIX_LEN); return true; } - char tmp[96]; + char tmp[FLOOD_RETRY_LIST_TEXT_MAX]; StrHelper::strncpy(tmp, value, sizeof(tmp)); - const char* parts[FLOOD_RETRY_BUCKET_PREFIXES + 1]; - int num = mesh::Utils::parseTextParts(tmp, parts, FLOOD_RETRY_BUCKET_PREFIXES + 1); + const char* parts[FLOOD_RETRY_LIST_PREFIXES + 1]; + int num = mesh::Utils::parseTextParts(tmp, parts, FLOOD_RETRY_LIST_PREFIXES + 1); if (num > max_prefixes) { return false; } @@ -436,7 +427,11 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->flood_retry_prefs_magic[0], sizeof(_prefs->flood_retry_prefs_magic)); // 302 file.read((uint8_t *)&_prefs->flood_retry_prefixes[0][0], sizeof(_prefs->flood_retry_prefixes)); // 304 file.read((uint8_t *)&_prefs->flood_retry_bridge_enabled, sizeof(_prefs->flood_retry_bridge_enabled)); // 328 + memset(_prefs->flood_retry_bridge_buckets, 0, sizeof(_prefs->flood_retry_bridge_buckets)); + memset(_prefs->flood_retry_ignore_prefixes, 0, sizeof(_prefs->flood_retry_ignore_prefixes)); file.read((uint8_t *)&_prefs->flood_retry_bridge_buckets[0][0][0], sizeof(_prefs->flood_retry_bridge_buckets)); // 329 + size_t flood_retry_ignore_read = file.read((uint8_t *)&_prefs->flood_retry_ignore_prefixes[0][0], + sizeof(_prefs->flood_retry_ignore_prefixes)); // 635 // PowerSaving-only prefs stored radio_fem_rxgain at 291, before direct retry timing existed. if (radio_fem_rxgain_read != sizeof(_prefs->radio_fem_rxgain) && legacy_retry_attempts_read == sizeof(legacy_retry_attempts_or_radio_fem_rxgain) @@ -444,7 +439,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1)) { _prefs->radio_fem_rxgain = constrain(legacy_retry_attempts_or_radio_fem_rxgain, 0, 1); } - // next: 473 + // next: 659 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -505,12 +500,16 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { memset(_prefs->flood_retry_prefixes, 0, sizeof(_prefs->flood_retry_prefixes)); _prefs->flood_retry_bridge_enabled = 0; memset(_prefs->flood_retry_bridge_buckets, 0, sizeof(_prefs->flood_retry_bridge_buckets)); + memset(_prefs->flood_retry_ignore_prefixes, 0, sizeof(_prefs->flood_retry_ignore_prefixes)); } else { _prefs->flood_retry_attempts = constrain(_prefs->flood_retry_attempts, FLOOD_RETRY_COUNT_MIN, FLOOD_RETRY_COUNT_MAX); if (_prefs->flood_retry_path_gate > 63 && _prefs->flood_retry_path_gate != FLOOD_RETRY_PATH_GATE_DISABLED) { _prefs->flood_retry_path_gate = floodRetryPresetPathDefault(_prefs->direct_retry_preset); } _prefs->flood_retry_bridge_enabled = constrain(_prefs->flood_retry_bridge_enabled, 0, 1); + if (flood_retry_ignore_read != sizeof(_prefs->flood_retry_ignore_prefixes)) { + memset(_prefs->flood_retry_ignore_prefixes, 0, sizeof(_prefs->flood_retry_ignore_prefixes)); + } } file.close(); @@ -591,7 +590,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->flood_retry_prefixes[0][0], sizeof(_prefs->flood_retry_prefixes)); // 304 file.write((uint8_t *)&_prefs->flood_retry_bridge_enabled, sizeof(_prefs->flood_retry_bridge_enabled)); // 328 file.write((uint8_t *)&_prefs->flood_retry_bridge_buckets[0][0][0], sizeof(_prefs->flood_retry_bridge_buckets)); // 329 - // next: 473 + file.write((uint8_t *)&_prefs->flood_retry_ignore_prefixes[0][0], sizeof(_prefs->flood_retry_ignore_prefixes)); // 635 + // next: 659 file.close(); } @@ -756,6 +756,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else if (memcmp(config, "flood.retry.prefixes", 20) == 0) { formatFloodRetryPrefixes(tmp, _prefs); sprintf(reply, "> %s", tmp[0] ? tmp : "none"); + } else if (memcmp(config, "flood.retry.ignore", 18) == 0) { + formatFloodRetryPrefixList(tmp, _prefs->flood_retry_ignore_prefixes, FLOOD_RETRY_IGNORE_PREFIXES); + sprintf(reply, "> %s", tmp[0] ? tmp : "none"); } else if (memcmp(config, "flood.retry.bridge", 18) == 0) { sprintf(reply, "> %s", _prefs->flood_retry_bridge_enabled ? "on" : "off"); } else if (memcmp(config, "flood.retry.bucket.", 19) == 0) { @@ -1061,6 +1064,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { strcpy(reply, "Error, use comma-separated 3-byte hex prefixes"); } + } else if (memcmp(config, "flood.retry.ignore ", 19) == 0) { + if (parseFloodRetryPrefixList(_prefs->flood_retry_ignore_prefixes, + FLOOD_RETRY_IGNORE_PREFIXES, &config[19])) { + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes", + (unsigned int)FLOOD_RETRY_IGNORE_PREFIXES); + } } else if (memcmp(config, "flood.retry.bridge ", 19) == 0) { if (memcmp(&config[19], "on", 2) == 0) { _prefs->flood_retry_bridge_enabled = 1; @@ -1084,7 +1096,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re savePrefs(); strcpy(reply, "OK"); } else { - strcpy(reply, "Error, use up to 8 comma-separated 3-byte hex prefixes"); + sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes", + (unsigned int)FLOOD_RETRY_BUCKET_PREFIXES); } } else if (memcmp(config, "direct.txdelay ", 15) == 0) { float f = atof(&config[15]); @@ -1674,6 +1687,15 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, use comma-separated 3-byte hex prefixes"); } + } else if (memcmp(config, "flood.retry.ignore ", 19) == 0) { + if (parseFloodRetryPrefixList(_prefs->flood_retry_ignore_prefixes, + FLOOD_RETRY_IGNORE_PREFIXES, &config[19])) { + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes", + (unsigned int)FLOOD_RETRY_IGNORE_PREFIXES); + } } else if (memcmp(config, "flood.retry.bridge ", 19) == 0) { if (memcmp(&config[19], "on", 2) == 0) { _prefs->flood_retry_bridge_enabled = 1; @@ -1697,7 +1719,8 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep savePrefs(); strcpy(reply, "OK"); } else { - strcpy(reply, "Error, use up to 8 comma-separated 3-byte hex prefixes"); + sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes", + (unsigned int)FLOOD_RETRY_BUCKET_PREFIXES); } } else if (memcmp(config, "direct.txdelay ", 15) == 0) { float f = atof(&config[15]); @@ -1947,6 +1970,9 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else if (memcmp(config, "flood.retry.prefixes", 20) == 0) { formatFloodRetryPrefixes(tmp, _prefs); sprintf(reply, "> %s", tmp[0] ? tmp : "none"); + } else if (memcmp(config, "flood.retry.ignore", 18) == 0) { + formatFloodRetryPrefixList(tmp, _prefs->flood_retry_ignore_prefixes, FLOOD_RETRY_IGNORE_PREFIXES); + sprintf(reply, "> %s", tmp[0] ? tmp : "none"); } else if (memcmp(config, "flood.retry.bridge", 18) == 0) { sprintf(reply, "> %s", _prefs->flood_retry_bridge_enabled ? "on" : "off"); } else if (memcmp(config, "flood.retry.bucket.", 19) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 321c3c8fa0..40806d4913 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -48,7 +48,19 @@ #define FLOOD_RETRY_BRIDGE_BUCKETS 6 #endif #ifndef FLOOD_RETRY_BUCKET_PREFIXES - #define FLOOD_RETRY_BUCKET_PREFIXES 8 + #define FLOOD_RETRY_BUCKET_PREFIXES 17 +#endif +#ifndef FLOOD_RETRY_IGNORE_PREFIXES + #define FLOOD_RETRY_IGNORE_PREFIXES 8 +#endif +#ifndef FLOOD_RETRY_LIST_PREFIXES + #define FLOOD_RETRY_LIST_PREFIXES ((FLOOD_RETRY_IGNORE_PREFIXES > FLOOD_RETRY_BUCKET_PREFIXES) ? FLOOD_RETRY_IGNORE_PREFIXES : FLOOD_RETRY_BUCKET_PREFIXES) +#endif +#ifndef FLOOD_RETRY_LIST_TEXT_MAX + #define FLOOD_RETRY_LIST_TEXT_MAX (FLOOD_RETRY_LIST_PREFIXES * FLOOD_RETRY_PREFIX_LEN * 2 + FLOOD_RETRY_LIST_PREFIXES) +#endif +#ifndef COMMON_CLI_TMP_LEN + #define COMMON_CLI_TMP_LEN ((FLOOD_RETRY_LIST_TEXT_MAX > (PRV_KEY_SIZE * 2 + 4)) ? FLOOD_RETRY_LIST_TEXT_MAX : (PRV_KEY_SIZE * 2 + 4)) #endif struct NodePrefs { // persisted to file @@ -107,6 +119,7 @@ struct NodePrefs { // persisted to file uint8_t flood_retry_prefixes[FLOOD_RETRY_PREFIX_SLOTS][FLOOD_RETRY_PREFIX_LEN]; uint8_t flood_retry_bridge_enabled; uint8_t flood_retry_bridge_buckets[FLOOD_RETRY_BRIDGE_BUCKETS][FLOOD_RETRY_BUCKET_PREFIXES][FLOOD_RETRY_PREFIX_LEN]; + uint8_t flood_retry_ignore_prefixes[FLOOD_RETRY_IGNORE_PREFIXES][FLOOD_RETRY_PREFIX_LEN]; }; class CommonCLICallbacks { @@ -166,7 +179,7 @@ class CommonCLI { SensorManager* _sensors; RegionMap* _region_map; ClientACL* _acl; - char tmp[PRV_KEY_SIZE*2 + 4]; + char tmp[COMMON_CLI_TMP_LEN]; mesh::RTCClock* getRTCClock() { return _rtc; } void savePrefs(); diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index ca81b144ff..2d2125dd1c 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -175,8 +175,6 @@ class SimpleMeshTables : public mesh::MeshTables { f.write((const uint8_t *) &_next_idx, sizeof(_next_idx)); f.write((const uint8_t *) &_acks[0], sizeof(_acks)); f.write((const uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx)); - f.write((const uint8_t *) &_recent_repeaters[0], sizeof(_recent_repeaters)); - f.write((const uint8_t *) &_next_recent_repeater_idx, sizeof(_next_recent_repeater_idx)); } #endif From ae67293bcc854684119fe230b2007da4caf641d0 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 8 May 2026 01:23:03 -0700 Subject: [PATCH 34/94] Honor flood retry ignore for echoes --- docs/cli_commands.md | 2 +- examples/simple_repeater/MyMesh.cpp | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 1a6e4233e5..9631a64a9e 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -850,7 +850,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Default:** empty -**Note:** Ignored prefixes do not count as a heard bridge bucket or as the implicit catch-all bucket when bridge retry decides whether every target has repeated the flood. +**Note:** In non-bridge retry, an echo whose last hop matches an ignored prefix does not cancel a queued retry as successful. In bridge mode, ignored prefixes do not count as a heard bridge bucket or as the implicit catch-all bucket when bridge retry decides whether every target has repeated the flood. --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 3fa770e91f..f6f515ee29 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1363,6 +1363,17 @@ bool MyMesh::isFloodRetryEchoTarget(const mesh::Packet* packet, uint8_t progress state->heard_mask |= floodRetryBridgeHeardMask(packet, state->source_bucket, state->progress_marker) & state->target_mask; return (state->heard_mask & state->target_mask) == state->target_mask; } + if (packet->getPathHashCount() == 0) { + return false; + } + uint8_t hash_size = packet->getPathHashSize(); + if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { + return false; + } + const uint8_t* heard_prefix = &packet->path[(packet->getPathHashCount() - 1) * hash_size]; + if (floodRetryPrefixIgnored(heard_prefix, hash_size)) { + return false; + } if (hasFloodRetryPrefixes()) { return floodRetryLastHopMatches(packet); } From b475bde7303ef745d56ee95527c96882612aca6f Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 8 May 2026 10:53:21 -0700 Subject: [PATCH 35/94] Unify retry preset settings --- docs/cli_commands.md | 45 ++-- docs/halo_keymind_settings.md | 308 ++++++++++++++++++++++++++++ examples/simple_repeater/MyMesh.cpp | 12 +- src/helpers/CommonCLI.cpp | 188 ++++++++--------- src/helpers/CommonCLI.h | 9 +- 5 files changed, 437 insertions(+), 125 deletions(-) create mode 100644 docs/halo_keymind_settings.md diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 9631a64a9e..c3cf2bd934 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -570,10 +570,10 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -#### View or change the direct retry timing preset +#### View or change the retry preset **Usage:** -- `get direct.retry.preset` -- `set direct.retry.preset ` +- `get retry.preset` +- `set retry.preset ` **Parameters:** - `value`: `infra`|`rooftop`|`mobile` or `0`|`1`|`2` @@ -581,11 +581,11 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Default:** `rooftop` (`1`) **Presets:** -- `infra` (`0`): `275 ms` base wait, `4` retries, `150 ms` added per retry, SNR gate is SF floor + `15 dB` -- `rooftop` (`1`): `175 ms` base wait, `15` retries, `100 ms` added per retry, SNR gate is SF floor + `5 dB` -- `mobile` (`2`): `175 ms` base wait, `15` retries, `50 ms` added per retry, SNR gate is the SF floor +- `infra` (`0`): `275 ms` direct base wait, `4` direct retries, `150 ms` added per direct retry, SNR gate is SF floor + `15 dB`; flood retry defaults to `1` retry and path gate `1` +- `rooftop` (`1`): `175 ms` direct base wait, `15` direct retries, `100 ms` added per direct retry, SNR gate is SF floor + `5 dB`; flood retry defaults to `3` retries and path gate `2` +- `mobile` (`2`): `175 ms` direct base wait, `15` direct retries, `50 ms` added per direct retry, SNR gate is the SF floor; flood retry defaults to `3` retries and path gate `1` -**Note:** Selecting a preset copies those values into the direct retry settings and also resets flood retry defaults. You can refine `direct.retry.margin`, `direct.retry.count`, `direct.retry.base`, `direct.retry.step`, `flood.retry.count`, or `flood.retry.path` afterward. Retry delay is `direct.txdelay` jitter + base wait + packet-length airtime wait + per-attempt step. +**Note:** Selecting a preset copies those values into the direct retry settings and resets flood retry defaults. You can refine `direct.retry.margin`, `direct.retry.count`, `direct.retry.base`, `direct.retry.step`, `flood.retry.count`, or `flood.retry.path` afterward. Retry delay is `direct.txdelay` jitter + base wait + packet-length airtime wait + per-attempt step. --- @@ -754,23 +754,6 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -#### View or change the flood retry preset -**Usage:** -- `get flood.retry.preset` -- `set flood.retry.preset ` - -**Parameters:** -- `value`: `infra`|`rooftop`|`mobile` or `0`|`1`|`2` - -**Presets:** -- `infra` (`0`): `1` retry, path gate `1` -- `rooftop` (`1`): `3` retries, path gate `2` -- `mobile` (`2`): `3` retries, path gate `1` - -**Note:** This applies only the flood retry defaults. `set direct.retry.preset` also resets these flood retry defaults. - ---- - #### View or change the number of flood retry attempts **Usage:** - `get flood.retry.count` @@ -797,6 +780,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change whether advert packets are flood-retried +**Usage:** +- `get flood.retry.advert` +- `set flood.retry.advert ` + +**Parameters:** +- `state`: `on` or `off` + +**Default:** `off` + +**Note:** When this is `off`, node advert packets (`PAYLOAD_TYPE_ADVERT`, type `4`) are not queued for flood retry. + +--- + #### View or change flood retry target prefixes **Usage:** - `get flood.retry.prefixes` diff --git a/docs/halo_keymind_settings.md b/docs/halo_keymind_settings.md new file mode 100644 index 0000000000..c451707f23 --- /dev/null +++ b/docs/halo_keymind_settings.md @@ -0,0 +1,308 @@ +# Halo and Keymind Branch Settings + +This file covers only CLI settings added by the Halo or Keymind branches. Use +`docs/cli_commands.md` for the general MeshCore CLI. + +## Quick Start + +Use this baseline when bringing a Halo or Keymind repeater onto the network: + +```text +set retry.preset rooftop +set direct.retry.heard on +set flood.retry.advert off +set flood.retry.bridge off +set flood.retry.prefixes none +set flood.retry.ignore none +``` + +Then verify: + +```text +get retry.preset +get direct.retry.heard +get flood.retry.advert +get flood.retry.prefixes +get flood.retry.ignore +``` + +Use prefixes from debug logs such as `path=7773D0>BEEBB0` or `heard=C7618C`. Prefix lists are comma-separated hex values, for example `71CE82,C7618C`. + +## Prefix Worksheet + +Keep Halo and Keymind prefixes in one place before programming buckets or ignore lists. + +| Network | Prefix | Node or site | Use | Notes | +| --- | --- | --- | --- | --- | +| Halo | `A1B2C3` | example remote relay | bucket/target | Replace with real prefix | +| Keymind | `71CE82` | example observed relay | ignore/target | Replace with real prefix | +| Keymind | `C7618C` | example observed relay | ignore/target | Replace with real prefix | + +## Common Examples + +Disable retrying advert packets: + +```text +set flood.retry.advert off +get flood.retry.advert +``` + +Ignore a relay as a successful flood retry echo: + +```text +set flood.retry.ignore 71CE82,C7618C +get flood.retry.ignore +``` + +Only accept specific downstream relays as flood retry success: + +```text +set flood.retry.prefixes BEEBB0,425E5C +get flood.retry.prefixes +``` + +Bridge two groups of repeaters: + +```text +set flood.retry.bridge on +set flood.retry.bucket 1 71CE82,C7618C +set flood.retry.bucket 2 BEEBB0,425E5C +get flood.retry.bucket.1 +get flood.retry.bucket.2 +``` + +Return to simple non-bridge flood retry: + +```text +set flood.retry.bridge off +set flood.retry.prefixes none +set flood.retry.ignore none +``` + +## Added Settings + +| Setting | What it does | How to use | Example | +| --- | --- | --- | --- | +| `recent.repeater` | Shows or seeds the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. | `get recent.repeater`, `get recent.repeater `, `set recent.repeater ` | `set recent.repeater A1B2C3 -8.5` | +| `radio.fem.rxgain` | Controls the external LoRa FEM receive-path LNA where the board supports it. | `get radio.fem.rxgain`, `set radio.fem.rxgain on/off` | `set radio.fem.rxgain on` | + +## Recent Repeater Table + +Direct retry uses the recent repeater table when `direct.retry.heard` is `on`. +Bridge buckets also use this table: a configured bucket prefix is active only +when it was heard within the last hour. + +Show learned rows: + +```text +get recent.repeater +get recent.repeater 2 +``` + +Seed or correct a prefix: + +```text +set recent.repeater A1B2C3 -8.5 +``` + +Rows are sorted by prefix width, then SNR. A full direct retry failure lowers +the matching row by `0.25 dB`. + +## Direct Retry Settings + +Direct retry applies to direct-routed packets. A queued resend is canceled when the next-hop echo is heard. + +| Setting | What it does | How to use | Example | +| --- | --- | --- | --- | +| `retry.preset` | Applies shared direct and flood retry defaults. Values: `infra`, `rooftop`, `mobile` or `0`, `1`, `2`. | `get retry.preset`, `set retry.preset ` | `set retry.preset rooftop` | +| `direct.retry.heard` | Uses the recent repeater table as the direct retry eligibility gate. | `get direct.retry.heard`, `set direct.retry.heard on/off` | `set direct.retry.heard on` | +| `direct.retry.margin` | SNR margin in dB above the SF-specific receive floor. | `get direct.retry.margin`, `set direct.retry.margin <0-40>` | `set direct.retry.margin 5` | +| `direct.retry.count` | Maximum direct retry attempts after initial TX. | `get direct.retry.count`, `set direct.retry.count <1-15>` | `set direct.retry.count 15` | +| `direct.retry.base` | Base wait in milliseconds before retry. | `get direct.retry.base`, `set direct.retry.base <10-5000>` | `set direct.retry.base 175` | +| `direct.retry.step` | Milliseconds added per retry attempt. | `get direct.retry.step`, `set direct.retry.step <0-5000>` | `set direct.retry.step 100` | + +Preset details: + +| Preset | Base | Count | Step | SNR gate | +| --- | ---: | ---: | ---: | --- | +| `infra` | `275 ms` | `4` | `150 ms` | SF floor + `15 dB` | +| `rooftop` | `175 ms` | `15` | `100 ms` | SF floor + `5 dB` | +| `mobile` | `175 ms` | `15` | `50 ms` | SF floor | + +Example for a quiet fixed repeater: + +```text +set retry.preset rooftop +set direct.retry.heard on +set direct.retry.margin 5 +``` + +Example for a moving or weak-link node: + +```text +set retry.preset mobile +set direct.retry.margin 0 +``` + +## Flood And Advert Settings + +Flood retry applies to flood-routed packets. A queued retry is canceled when a qualifying downstream echo is heard. + +| Setting | What it does | How to use | Example | +| --- | --- | --- | --- | +| `flood.retry.count` | Maximum flood retry attempts after initial TX. `0` disables flood retry. | `get flood.retry.count`, `set flood.retry.count <0-3>` | `set flood.retry.count 3` | +| `flood.retry.path` | Maximum path hash count eligible for flood retry, or `off` to disable the gate. | `get flood.retry.path`, `set flood.retry.path <0-63/off>` | `set flood.retry.path 1` | +| `flood.retry.advert` | Allows or blocks retry for node advert packets (`type=4`). Default is `off`. | `get flood.retry.advert`, `set flood.retry.advert on/off` | `set flood.retry.advert off` | +| `flood.retry.prefixes` | Target prefixes. If set, only matching downstream echoes cancel a retry. | `get flood.retry.prefixes`, `set flood.retry.prefixes ` | `set flood.retry.prefixes BEEBB0,425E5C` | +| `flood.retry.ignore` | Ignored prefixes. In non-bridge retry, ignored last-hop echoes do not cancel retry. | `get flood.retry.ignore`, `set flood.retry.ignore ` | `set flood.retry.ignore 71CE82,C7618C` | +| `flood.retry.bridge` | Enables bucket-based bridge retry logic. | `get flood.retry.bridge`, `set flood.retry.bridge on/off` | `set flood.retry.bridge on` | +| `flood.retry.bucket.` | Shows one bridge bucket. Buckets are numbered `1`-`6`. | `get flood.retry.bucket.` | `get flood.retry.bucket.1` | +| `flood.retry.bucket` | Sets bridge bucket prefixes. | `set flood.retry.bucket <1-6> ` | `set flood.retry.bucket 1 71CE82,C7618C` | + +The shared retry preset sets these flood defaults: + +| Preset | Retry count | Path gate | +| --- | ---: | ---: | +| `infra` | `1` | `1` | +| `rooftop` | `3` | `2` | +| `mobile` | `3` | `1` | + +Example for Keymind path-gated retry: + +```text +set retry.preset rooftop +set flood.retry.path 1 +set flood.retry.advert off +set flood.retry.ignore 71CE82,C7618C +``` + +Example for Halo targeted retry: + +```text +set flood.retry.bridge off +set flood.retry.prefixes A1B2C3,D4E5F6 +set flood.retry.ignore none +``` + +Example for Halo/Keymind bridge retry: + +```text +set flood.retry.bridge on +set flood.retry.bucket 1 A1B2C3,D4E5F6 +set flood.retry.bucket 2 71CE82,C7618C +set flood.retry.advert off +``` + +## North South Buckets + +Buckets describe groups of repeaters on different sides of this relay. Bucket +numbers do not have built-in meanings; this example uses bucket `1` for North +and bucket `2` for South. + +```text + North bucket 1 + +-----------------------+ + | A1B2C3 D4E5F6 | + | North A North B | + +-----------+-----------+ + | + v + +-----------+ + | This node | + +-----------+ + ^ + | + +-----------+-----------+ + | 71CE82 C7618C | + | South A South B | + +-----------------------+ + South bucket 2 +``` + +Configure the buckets: + +```text +set flood.retry.bridge on +set flood.retry.bucket 1 A1B2C3,D4E5F6 +set flood.retry.bucket 2 71CE82,C7618C +set flood.retry.ignore none +``` + +Packet heard from the North: + +```text + heard source + | + v + +--------------+ retry targets + | North bucket | -----> South bucket + | bucket 1 | -----> Other fresh/unbucketed relays + +--------------+ +``` + +Packet heard from the South: + +```text + heard source + | + v + +--------------+ retry targets + | South bucket | -----> North bucket + | bucket 2 | -----> Other fresh/unbucketed relays + +--------------+ +``` + +Packet heard from an unbucketed or pathless source: + +```text + heard source + | + v + +--------------+ retry targets + | Other bucket | -----> North bucket + | implicit | -----> South bucket + +--------------+ +``` + +Bridge retry stays eligible until every target bucket has been heard or +`flood.retry.count` is exhausted. A configured bucket is a target only when at +least one of its prefixes is fresh in `recent.repeater`. Prefixes in +`flood.retry.ignore` never count as bucket hits. + +## Troubleshooting + +If advert packets are still retrying: + +```text +get flood.retry.advert +set flood.retry.advert off +``` + +If ignored prefixes still appear in `flood retry good` logs: + +```text +get flood.retry.ignore +set flood.retry.ignore +``` + +The ignored prefix must match the last hop shown as `heard=`. For example, this log needs `C7618C` in the ignore list: + +```text +flood retry good (... path=7773D0>C7618C, heard=C7618C ...) +``` + +If retries are too aggressive: + +```text +set flood.retry.count 1 +set flood.retry.path 1 +set direct.retry.count 4 +``` + +If retries are too sparse: + +```text +set retry.preset rooftop +set flood.retry.count 3 +set flood.retry.path 2 +``` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index f6f515ee29..425059174f 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -847,10 +847,10 @@ bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_ho return true; } uint8_t MyMesh::getDirectRetryPreset() const { - if (_prefs.direct_retry_preset <= DIRECT_RETRY_PRESET_MOBILE) { - return _prefs.direct_retry_preset; + if (_prefs.retry_preset <= RETRY_PRESET_MOBILE) { + return _prefs.retry_preset; } - return DIRECT_RETRY_PRESET_ROOFTOP; + return RETRY_PRESET_ROOFTOP; } uint8_t MyMesh::getDirectRetryConfiguredMaxAttempts() const { return constrain(_prefs.direct_retry_attempts, (uint8_t)1, (uint8_t)15); @@ -1099,6 +1099,9 @@ bool MyMesh::allowFloodRetry(const mesh::Packet* packet) const { if (_prefs.disable_fwd || constrain(_prefs.flood_retry_attempts, (uint8_t)0, (uint8_t)3) == 0) { return false; } + if (packet != NULL && packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && !_prefs.flood_retry_advert_enabled) { + return false; + } if (!_prefs.flood_retry_bridge_enabled) { return true; } @@ -1750,10 +1753,11 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT; _prefs.direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS; _prefs.direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS; - _prefs.direct_retry_preset = DIRECT_RETRY_PRESET_ROOFTOP; + _prefs.retry_preset = RETRY_PRESET_ROOFTOP; _prefs.flood_retry_attempts = 3; _prefs.flood_retry_path_gate = 2; _prefs.flood_retry_bridge_enabled = 0; + _prefs.flood_retry_advert_enabled = 0; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; _prefs.node_lon = ADVERT_LON; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index ab4513ca33..136451cdf0 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -29,11 +29,12 @@ #define DIRECT_RETRY_STEP_MS_DEFAULT DIRECT_RETRY_ROOFTOP_STEP_MS #define DIRECT_RETRY_STEP_MS_MIN 0 #define DIRECT_RETRY_STEP_MS_MAX 5000 -#define DIRECT_RETRY_PRESET_DEFAULT DIRECT_RETRY_PRESET_ROOFTOP +#define RETRY_PRESET_DEFAULT RETRY_PRESET_ROOFTOP #define FLOOD_RETRY_PREFS_MAGIC_0 0xF4 #define FLOOD_RETRY_PREFS_MAGIC_1 0x52 #define FLOOD_RETRY_COUNT_MIN 0 #define FLOOD_RETRY_COUNT_MAX 3 +#define FLOOD_RETRY_ADVERT_DEFAULT 0 // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { @@ -78,20 +79,20 @@ static float directRetryMarginX4ToDb(uint8_t margin_x4) { return ((float)margin_x4) / 4.0f; } -static uint8_t directRetryPresetOrDefault(uint8_t preset) { - if (preset <= DIRECT_RETRY_PRESET_MOBILE) { +static uint8_t retryPresetOrDefault(uint8_t preset) { + if (preset <= RETRY_PRESET_MOBILE) { return preset; } - return DIRECT_RETRY_PRESET_DEFAULT; + return RETRY_PRESET_DEFAULT; } -static const char* directRetryPresetName(uint8_t preset) { - switch (directRetryPresetOrDefault(preset)) { - case DIRECT_RETRY_PRESET_INFRA: +static const char* retryPresetName(uint8_t preset) { + switch (retryPresetOrDefault(preset)) { + case RETRY_PRESET_INFRA: return "infra"; - case DIRECT_RETRY_PRESET_MOBILE: + case RETRY_PRESET_MOBILE: return "mobile"; - case DIRECT_RETRY_PRESET_ROOFTOP: + case RETRY_PRESET_ROOFTOP: default: return "rooftop"; } @@ -113,36 +114,36 @@ static uint8_t directRetryEffectiveMarginX4(const NodePrefs* prefs) { return constrain(prefs->direct_retry_snr_margin_db, (uint8_t)0, (uint8_t)DIRECT_RETRY_SNR_MARGIN_X4_MAX); } -static uint16_t directRetryPresetStepDefault(uint8_t preset) { - switch (directRetryPresetOrDefault(preset)) { - case DIRECT_RETRY_PRESET_INFRA: +static uint16_t retryPresetStepDefault(uint8_t preset) { + switch (retryPresetOrDefault(preset)) { + case RETRY_PRESET_INFRA: return DIRECT_RETRY_INFRA_STEP_MS; - case DIRECT_RETRY_PRESET_MOBILE: + case RETRY_PRESET_MOBILE: return DIRECT_RETRY_MOBILE_STEP_MS; - case DIRECT_RETRY_PRESET_ROOFTOP: + case RETRY_PRESET_ROOFTOP: default: return DIRECT_RETRY_ROOFTOP_STEP_MS; } } static uint8_t floodRetryPresetCountDefault(uint8_t preset) { - switch (directRetryPresetOrDefault(preset)) { - case DIRECT_RETRY_PRESET_INFRA: + switch (retryPresetOrDefault(preset)) { + case RETRY_PRESET_INFRA: return 1; - case DIRECT_RETRY_PRESET_MOBILE: - case DIRECT_RETRY_PRESET_ROOFTOP: + case RETRY_PRESET_MOBILE: + case RETRY_PRESET_ROOFTOP: default: return 3; } } static uint8_t floodRetryPresetPathDefault(uint8_t preset) { - switch (directRetryPresetOrDefault(preset)) { - case DIRECT_RETRY_PRESET_INFRA: + switch (retryPresetOrDefault(preset)) { + case RETRY_PRESET_INFRA: return 1; - case DIRECT_RETRY_PRESET_MOBILE: + case RETRY_PRESET_MOBILE: return 1; - case DIRECT_RETRY_PRESET_ROOFTOP: + case RETRY_PRESET_ROOFTOP: default: return 2; } @@ -157,33 +158,22 @@ static uint8_t floodRetryEffectiveCount(const NodePrefs* prefs) { return constrain(prefs->flood_retry_attempts, (uint8_t)FLOOD_RETRY_COUNT_MIN, (uint8_t)FLOOD_RETRY_COUNT_MAX); } -static uint8_t floodRetryPresetForPrefs(const NodePrefs* prefs) { - uint8_t count = floodRetryEffectiveCount(prefs); - uint8_t path_gate = prefs->flood_retry_path_gate; - for (uint8_t preset = DIRECT_RETRY_PRESET_INFRA; preset <= DIRECT_RETRY_PRESET_MOBILE; preset++) { - if (count == floodRetryPresetCountDefault(preset) && path_gate == floodRetryPresetPathDefault(preset)) { - return preset; - } - } - return directRetryPresetOrDefault(prefs->direct_retry_preset); -} - -static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) { - prefs->direct_retry_preset = directRetryPresetOrDefault(preset); - switch (prefs->direct_retry_preset) { - case DIRECT_RETRY_PRESET_INFRA: +static void applyRetryPreset(NodePrefs* prefs, uint8_t preset) { + prefs->retry_preset = retryPresetOrDefault(preset); + switch (prefs->retry_preset) { + case RETRY_PRESET_INFRA: prefs->direct_retry_base_ms = DIRECT_RETRY_INFRA_BASE_MS; prefs->direct_retry_attempts = DIRECT_RETRY_INFRA_COUNT; prefs->direct_retry_step_ms = DIRECT_RETRY_INFRA_STEP_MS; prefs->direct_retry_snr_margin_db = DIRECT_RETRY_INFRA_MARGIN_X4; break; - case DIRECT_RETRY_PRESET_MOBILE: + case RETRY_PRESET_MOBILE: prefs->direct_retry_base_ms = DIRECT_RETRY_MOBILE_BASE_MS; prefs->direct_retry_attempts = DIRECT_RETRY_MOBILE_COUNT; prefs->direct_retry_step_ms = DIRECT_RETRY_MOBILE_STEP_MS; prefs->direct_retry_snr_margin_db = DIRECT_RETRY_MOBILE_MARGIN_X4; break; - case DIRECT_RETRY_PRESET_ROOFTOP: + case RETRY_PRESET_ROOFTOP: default: prefs->direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS; prefs->direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT; @@ -191,7 +181,13 @@ static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) { prefs->direct_retry_snr_margin_db = DIRECT_RETRY_ROOFTOP_MARGIN_X4; break; } - applyFloodRetryPreset(prefs, prefs->direct_retry_preset); + applyFloodRetryPreset(prefs, prefs->retry_preset); +} + +static void formatRetryPreset(char* reply, const NodePrefs* prefs) { + sprintf(reply, "> %d,%s", + (uint32_t)retryPresetOrDefault(prefs->retry_preset), + retryPresetName(prefs->retry_preset)); } static bool parseFloodRetryPathGate(const char* value, uint8_t& path_gate) { @@ -316,21 +312,21 @@ static bool parseFloodRetryPrefixList(uint8_t dest[][FLOOD_RETRY_PREFIX_LEN], ui return true; } -static bool parseDirectRetryPreset(const char* value, uint8_t& preset) { +static bool parseRetryPreset(const char* value, uint8_t& preset) { if (value == NULL) { return false; } if (strcmp(value, "0") == 0 || strcmp(value, "infra") == 0 || strcmp(value, "infa") == 0 || strcmp(value, "infrastructure") == 0) { - preset = DIRECT_RETRY_PRESET_INFRA; + preset = RETRY_PRESET_INFRA; return true; } if (strcmp(value, "1") == 0 || strcmp(value, "rooftop") == 0) { - preset = DIRECT_RETRY_PRESET_ROOFTOP; + preset = RETRY_PRESET_ROOFTOP; return true; } if (strcmp(value, "2") == 0 || strcmp(value, "mobile") == 0) { - preset = DIRECT_RETRY_PRESET_MOBILE; + preset = RETRY_PRESET_MOBILE; return true; } return false; @@ -418,7 +414,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->direct_retry_timing_magic[0], sizeof(_prefs->direct_retry_timing_magic)); // 294 size_t radio_fem_rxgain_read = file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 296 - file.read((uint8_t *)&_prefs->direct_retry_preset, sizeof(_prefs->direct_retry_preset)); // 297 + file.read((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 297 size_t retry_step_read = file.read((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 298 size_t flood_retry_attempts_read = file.read((uint8_t *)&_prefs->flood_retry_attempts, @@ -432,6 +428,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->flood_retry_bridge_buckets[0][0][0], sizeof(_prefs->flood_retry_bridge_buckets)); // 329 size_t flood_retry_ignore_read = file.read((uint8_t *)&_prefs->flood_retry_ignore_prefixes[0][0], sizeof(_prefs->flood_retry_ignore_prefixes)); // 635 + _prefs->flood_retry_advert_enabled = FLOOD_RETRY_ADVERT_DEFAULT; + size_t flood_retry_advert_read = file.read((uint8_t *)&_prefs->flood_retry_advert_enabled, + sizeof(_prefs->flood_retry_advert_enabled)); // 659 // PowerSaving-only prefs stored radio_fem_rxgain at 291, before direct retry timing existed. if (radio_fem_rxgain_read != sizeof(_prefs->radio_fem_rxgain) && legacy_retry_attempts_read == sizeof(legacy_retry_attempts_or_radio_fem_rxgain) @@ -487,29 +486,35 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // sanitise settings _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean _prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean - _prefs->direct_retry_preset = directRetryPresetOrDefault(_prefs->direct_retry_preset); + _prefs->retry_preset = retryPresetOrDefault(_prefs->retry_preset); if (retry_step_read != sizeof(_prefs->direct_retry_step_ms)) { - _prefs->direct_retry_step_ms = directRetryPresetStepDefault(_prefs->direct_retry_preset); + _prefs->direct_retry_step_ms = retryPresetStepDefault(_prefs->retry_preset); } else { _prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); } if (flood_retry_attempts_read != sizeof(_prefs->flood_retry_attempts) || _prefs->flood_retry_prefs_magic[0] != FLOOD_RETRY_PREFS_MAGIC_0 || _prefs->flood_retry_prefs_magic[1] != FLOOD_RETRY_PREFS_MAGIC_1) { - applyFloodRetryPreset(_prefs, _prefs->direct_retry_preset); + applyFloodRetryPreset(_prefs, _prefs->retry_preset); memset(_prefs->flood_retry_prefixes, 0, sizeof(_prefs->flood_retry_prefixes)); _prefs->flood_retry_bridge_enabled = 0; memset(_prefs->flood_retry_bridge_buckets, 0, sizeof(_prefs->flood_retry_bridge_buckets)); memset(_prefs->flood_retry_ignore_prefixes, 0, sizeof(_prefs->flood_retry_ignore_prefixes)); + _prefs->flood_retry_advert_enabled = FLOOD_RETRY_ADVERT_DEFAULT; } else { _prefs->flood_retry_attempts = constrain(_prefs->flood_retry_attempts, FLOOD_RETRY_COUNT_MIN, FLOOD_RETRY_COUNT_MAX); if (_prefs->flood_retry_path_gate > 63 && _prefs->flood_retry_path_gate != FLOOD_RETRY_PATH_GATE_DISABLED) { - _prefs->flood_retry_path_gate = floodRetryPresetPathDefault(_prefs->direct_retry_preset); + _prefs->flood_retry_path_gate = floodRetryPresetPathDefault(_prefs->retry_preset); } _prefs->flood_retry_bridge_enabled = constrain(_prefs->flood_retry_bridge_enabled, 0, 1); if (flood_retry_ignore_read != sizeof(_prefs->flood_retry_ignore_prefixes)) { memset(_prefs->flood_retry_ignore_prefixes, 0, sizeof(_prefs->flood_retry_ignore_prefixes)); } + if (flood_retry_advert_read != sizeof(_prefs->flood_retry_advert_enabled)) { + _prefs->flood_retry_advert_enabled = FLOOD_RETRY_ADVERT_DEFAULT; + } else { + _prefs->flood_retry_advert_enabled = constrain(_prefs->flood_retry_advert_enabled, 0, 1); + } } file.close(); @@ -581,7 +586,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { uint8_t retry_timing_magic[2] = { DIRECT_RETRY_TIMING_MAGIC_0, DIRECT_RETRY_TIMING_MAGIC_1 }; file.write(retry_timing_magic, sizeof(retry_timing_magic)); // 294 file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 296 - file.write((uint8_t *)&_prefs->direct_retry_preset, sizeof(_prefs->direct_retry_preset)); // 297 + file.write((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 297 file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 298 file.write((uint8_t *)&_prefs->flood_retry_attempts, sizeof(_prefs->flood_retry_attempts)); // 300 file.write((uint8_t *)&_prefs->flood_retry_path_gate, sizeof(_prefs->flood_retry_path_gate)); // 301 @@ -591,7 +596,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->flood_retry_bridge_enabled, sizeof(_prefs->flood_retry_bridge_enabled)); // 328 file.write((uint8_t *)&_prefs->flood_retry_bridge_buckets[0][0][0], sizeof(_prefs->flood_retry_bridge_buckets)); // 329 file.write((uint8_t *)&_prefs->flood_retry_ignore_prefixes[0][0], sizeof(_prefs->flood_retry_ignore_prefixes)); // 635 - // next: 659 + file.write((uint8_t *)&_prefs->flood_retry_advert_enabled, sizeof(_prefs->flood_retry_advert_enabled)); // 659 + // next: 660 file.close(); } @@ -747,6 +753,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor)); } else if (memcmp(config, "flood.max", 9) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); + } else if (memcmp(config, "retry.preset", 12) == 0) { + formatRetryPreset(reply, _prefs); } else if (memcmp(config, "flood.retry.count", 17) == 0) { sprintf(reply, "> %d", (uint32_t)floodRetryEffectiveCount(_prefs)); } else if (memcmp(config, "flood.retry.path", 16) == 0) { @@ -759,6 +767,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else if (memcmp(config, "flood.retry.ignore", 18) == 0) { formatFloodRetryPrefixList(tmp, _prefs->flood_retry_ignore_prefixes, FLOOD_RETRY_IGNORE_PREFIXES); sprintf(reply, "> %s", tmp[0] ? tmp : "none"); + } else if (memcmp(config, "flood.retry.advert", 18) == 0) { + sprintf(reply, "> %s", _prefs->flood_retry_advert_enabled ? "on" : "off"); } else if (memcmp(config, "flood.retry.bridge", 18) == 0) { sprintf(reply, "> %s", _prefs->flood_retry_bridge_enabled ? "on" : "off"); } else if (memcmp(config, "flood.retry.bucket.", 19) == 0) { @@ -769,21 +779,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { sprintf(reply, "Error, bucket 1-%d", FLOOD_RETRY_BRIDGE_BUCKETS); } - } else if (memcmp(config, "flood.retry.preset", 18) == 0) { - uint8_t preset = floodRetryPresetForPrefs(_prefs); - sprintf(reply, "> %d,%s", - (uint32_t)preset, - directRetryPresetName(preset)); } else if (memcmp(config, "direct.txdelay", 14) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); } else if (memcmp(config, "direct.retry.heard", 18) == 0) { sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); } else if (memcmp(config, "direct.retry.margin", 19) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(directRetryMarginX4ToDb(directRetryEffectiveMarginX4(_prefs)))); - } else if (memcmp(config, "direct.retry.preset", 19) == 0) { - sprintf(reply, "> %d,%s", - (uint32_t)directRetryPresetOrDefault(_prefs->direct_retry_preset), - directRetryPresetName(_prefs->direct_retry_preset)); } else if (memcmp(config, "direct.retry.count", 18) == 0) { sprintf(reply, "> %d", (uint32_t)directRetryEffectiveCount(_prefs)); } else if (memcmp(config, "direct.retry.base", 17) == 0) { @@ -1030,10 +1031,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { strcpy(reply, "Error, max 64"); } - } else if (memcmp(config, "flood.retry.preset ", 19) == 0) { + } else if (memcmp(config, "retry.preset ", 13) == 0) { uint8_t preset; - if (parseDirectRetryPreset(&config[19], preset)) { - applyFloodRetryPreset(_prefs, preset); + if (parseRetryPreset(&config[13], preset)) { + applyRetryPreset(_prefs, preset); savePrefs(); strcpy(reply, "OK"); } else { @@ -1073,6 +1074,18 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes", (unsigned int)FLOOD_RETRY_IGNORE_PREFIXES); } + } else if (memcmp(config, "flood.retry.advert ", 19) == 0) { + if (memcmp(&config[19], "on", 2) == 0) { + _prefs->flood_retry_advert_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(&config[19], "off", 3) == 0) { + _prefs->flood_retry_advert_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be on or off"); + } } else if (memcmp(config, "flood.retry.bridge ", 19) == 0) { if (memcmp(&config[19], "on", 2) == 0) { _prefs->flood_retry_bridge_enabled = 1; @@ -1129,15 +1142,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { sprintf(reply, "Error, min 0 and max %d", DIRECT_RETRY_SNR_MARGIN_DB_MAX); } - } else if (memcmp(config, "direct.retry.preset ", 20) == 0) { - uint8_t preset; - if (parseDirectRetryPreset(&config[20], preset)) { - applyDirectRetryPreset(_prefs, preset); - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be infra, rooftop, mobile, 0, 1, or 2"); - } } else if (memcmp(config, "direct.retry.count ", 19) == 0) { int count = atoi(&config[19]); if (count >= DIRECT_RETRY_COUNT_MIN && count <= DIRECT_RETRY_COUNT_MAX) { @@ -1653,10 +1657,10 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, max 64"); } - } else if (memcmp(config, "flood.retry.preset ", 19) == 0) { + } else if (memcmp(config, "retry.preset ", 13) == 0) { uint8_t preset; - if (parseDirectRetryPreset(&config[19], preset)) { - applyFloodRetryPreset(_prefs, preset); + if (parseRetryPreset(&config[13], preset)) { + applyRetryPreset(_prefs, preset); savePrefs(); strcpy(reply, "OK"); } else { @@ -1696,6 +1700,18 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes", (unsigned int)FLOOD_RETRY_IGNORE_PREFIXES); } + } else if (memcmp(config, "flood.retry.advert ", 19) == 0) { + if (memcmp(&config[19], "on", 2) == 0) { + _prefs->flood_retry_advert_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(&config[19], "off", 3) == 0) { + _prefs->flood_retry_advert_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be on or off"); + } } else if (memcmp(config, "flood.retry.bridge ", 19) == 0) { if (memcmp(&config[19], "on", 2) == 0) { _prefs->flood_retry_bridge_enabled = 1; @@ -1752,15 +1768,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { sprintf(reply, "Error, min 0 and max %d", DIRECT_RETRY_SNR_MARGIN_DB_MAX); } - } else if (memcmp(config, "direct.retry.preset ", 20) == 0) { - uint8_t preset; - if (parseDirectRetryPreset(&config[20], preset)) { - applyDirectRetryPreset(_prefs, preset); - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be infra, rooftop, mobile, 0, 1, or 2"); - } } else if (memcmp(config, "direct.retry.count ", 19) == 0) { int count = atoi(&config[19]); if (count >= DIRECT_RETRY_COUNT_MIN && count <= DIRECT_RETRY_COUNT_MAX) { @@ -1961,6 +1968,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor)); } else if (memcmp(config, "flood.max", 9) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); + } else if (memcmp(config, "retry.preset", 12) == 0) { + formatRetryPreset(reply, _prefs); } else if (memcmp(config, "flood.retry.count", 17) == 0) { sprintf(reply, "> %d", (uint32_t)floodRetryEffectiveCount(_prefs)); } else if (memcmp(config, "flood.retry.path", 16) == 0) { @@ -1973,6 +1982,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else if (memcmp(config, "flood.retry.ignore", 18) == 0) { formatFloodRetryPrefixList(tmp, _prefs->flood_retry_ignore_prefixes, FLOOD_RETRY_IGNORE_PREFIXES); sprintf(reply, "> %s", tmp[0] ? tmp : "none"); + } else if (memcmp(config, "flood.retry.advert", 18) == 0) { + sprintf(reply, "> %s", _prefs->flood_retry_advert_enabled ? "on" : "off"); } else if (memcmp(config, "flood.retry.bridge", 18) == 0) { sprintf(reply, "> %s", _prefs->flood_retry_bridge_enabled ? "on" : "off"); } else if (memcmp(config, "flood.retry.bucket.", 19) == 0) { @@ -1983,21 +1994,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else { sprintf(reply, "Error, bucket 1-%d", FLOOD_RETRY_BRIDGE_BUCKETS); } - } else if (memcmp(config, "flood.retry.preset", 18) == 0) { - uint8_t preset = floodRetryPresetForPrefs(_prefs); - sprintf(reply, "> %d,%s", - (uint32_t)preset, - directRetryPresetName(preset)); } else if (memcmp(config, "direct.txdelay", 14) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); } else if (memcmp(config, "direct.retry.heard", 18) == 0) { sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); } else if (memcmp(config, "direct.retry.margin", 19) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(directRetryMarginX4ToDb(directRetryEffectiveMarginX4(_prefs)))); - } else if (memcmp(config, "direct.retry.preset", 19) == 0) { - sprintf(reply, "> %d,%s", - (uint32_t)directRetryPresetOrDefault(_prefs->direct_retry_preset), - directRetryPresetName(_prefs->direct_retry_preset)); } else if (memcmp(config, "direct.retry.count", 18) == 0) { sprintf(reply, "> %d", (uint32_t)directRetryEffectiveCount(_prefs)); } else if (memcmp(config, "direct.retry.base", 17) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 40806d4913..5c1eb62fc4 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -19,9 +19,9 @@ #define LOOP_DETECT_MODERATE 2 #define LOOP_DETECT_STRICT 3 -#define DIRECT_RETRY_PRESET_INFRA 0 -#define DIRECT_RETRY_PRESET_ROOFTOP 1 -#define DIRECT_RETRY_PRESET_MOBILE 2 +#define RETRY_PRESET_INFRA 0 +#define RETRY_PRESET_ROOFTOP 1 +#define RETRY_PRESET_MOBILE 2 #define DIRECT_RETRY_INFRA_BASE_MS 275 #define DIRECT_RETRY_INFRA_COUNT 4 @@ -111,7 +111,7 @@ struct NodePrefs { // persisted to file uint8_t direct_retry_attempts; uint16_t direct_retry_base_ms; uint8_t direct_retry_timing_magic[2]; - uint8_t direct_retry_preset; + uint8_t retry_preset; uint16_t direct_retry_step_ms; uint8_t flood_retry_attempts; uint8_t flood_retry_path_gate; @@ -120,6 +120,7 @@ struct NodePrefs { // persisted to file uint8_t flood_retry_bridge_enabled; uint8_t flood_retry_bridge_buckets[FLOOD_RETRY_BRIDGE_BUCKETS][FLOOD_RETRY_BUCKET_PREFIXES][FLOOD_RETRY_PREFIX_LEN]; uint8_t flood_retry_ignore_prefixes[FLOOD_RETRY_IGNORE_PREFIXES][FLOOD_RETRY_PREFIX_LEN]; + uint8_t flood_retry_advert_enabled; }; class CommonCLICallbacks { From c244effb52ba90fd30cb1fe5ba2ecfa8f979e340 Mon Sep 17 00:00:00 2001 From: mikecarper <135079168+mikecarper@users.noreply.github.com> Date: Fri, 8 May 2026 11:06:24 -0700 Subject: [PATCH 36/94] Revise Halo and Keymind settings documentation Updated settings and examples for Halo and Keymind configuration, including changes to retry settings and prefix handling. --- docs/halo_keymind_settings.md | 47 ++++++++--------------------------- 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/docs/halo_keymind_settings.md b/docs/halo_keymind_settings.md index c451707f23..908c390ca3 100644 --- a/docs/halo_keymind_settings.md +++ b/docs/halo_keymind_settings.md @@ -5,7 +5,6 @@ This file covers only CLI settings added by the Halo or Keymind branches. Use ## Quick Start -Use this baseline when bringing a Halo or Keymind repeater onto the network: ```text set retry.preset rooftop @@ -26,17 +25,7 @@ get flood.retry.prefixes get flood.retry.ignore ``` -Use prefixes from debug logs such as `path=7773D0>BEEBB0` or `heard=C7618C`. Prefix lists are comma-separated hex values, for example `71CE82,C7618C`. - -## Prefix Worksheet - -Keep Halo and Keymind prefixes in one place before programming buckets or ignore lists. - -| Network | Prefix | Node or site | Use | Notes | -| --- | --- | --- | --- | --- | -| Halo | `A1B2C3` | example remote relay | bucket/target | Replace with real prefix | -| Keymind | `71CE82` | example observed relay | ignore/target | Replace with real prefix | -| Keymind | `C7618C` | example observed relay | ignore/target | Replace with real prefix | +Use prefixes from the analyzer or neighbors list or `get recent.repeater` after the repeater has been online for a few hours. ## Common Examples @@ -47,21 +36,23 @@ set flood.retry.advert off get flood.retry.advert ``` -Ignore a relay as a successful flood retry echo: +Ignore a repeater as a successful flood retry echo: +Use this if you have a car repeater and a house repeater; have the house ignore the car. ```text set flood.retry.ignore 71CE82,C7618C get flood.retry.ignore ``` -Only accept specific downstream relays as flood retry success: +Only accept specific downstream relays as flood retry success: +You're in a hole and need to hit a mountain top repeater to get out; keep trying till one you see one of these send out your packet. ```text -set flood.retry.prefixes BEEBB0,425E5C +set flood.retry.prefixes A58296,860CCA,425E5C get flood.retry.prefixes ``` -Bridge two groups of repeaters: +Bridge two groups of repeaters: ```text set flood.retry.bridge on @@ -102,7 +93,7 @@ get recent.repeater 2 Seed or correct a prefix: ```text -set recent.repeater A1B2C3 -8.5 +set recent.repeater A1B2C3 8.5 ``` Rows are sorted by prefix width, then SNR. A full direct retry failure lowers @@ -167,7 +158,7 @@ The shared retry preset sets these flood defaults: | `rooftop` | `3` | `2` | | `mobile` | `3` | `1` | -Example for Keymind path-gated retry: +Example for path-gated retry: ```text set retry.preset rooftop @@ -176,23 +167,6 @@ set flood.retry.advert off set flood.retry.ignore 71CE82,C7618C ``` -Example for Halo targeted retry: - -```text -set flood.retry.bridge off -set flood.retry.prefixes A1B2C3,D4E5F6 -set flood.retry.ignore none -``` - -Example for Halo/Keymind bridge retry: - -```text -set flood.retry.bridge on -set flood.retry.bucket 1 A1B2C3,D4E5F6 -set flood.retry.bucket 2 71CE82,C7618C -set flood.retry.advert off -``` - ## North South Buckets Buckets describe groups of repeaters on different sides of this relay. Bucket @@ -302,7 +276,6 @@ set direct.retry.count 4 If retries are too sparse: ```text -set retry.preset rooftop -set flood.retry.count 3 +set flood.retry.count 7 set flood.retry.path 2 ``` From bb8e7140a91557c3ec69d3b153d2448fae6c983f Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 8 May 2026 12:37:08 -0700 Subject: [PATCH 37/94] Add adaptive CR for direct retries --- docs/cli_commands.md | 18 +++ examples/simple_repeater/MyMesh.cpp | 52 ++++++++ examples/simple_repeater/MyMesh.h | 3 + src/Dispatcher.cpp | 23 ++++ src/Dispatcher.h | 10 ++ src/Mesh.cpp | 11 +- src/Mesh.h | 5 + src/Packet.cpp | 4 +- src/Packet.h | 1 + src/helpers/CommonCLI.cpp | 132 ++++++++++++++++++- src/helpers/CommonCLI.h | 9 ++ src/helpers/radiolib/CustomLLCC68Wrapper.h | 9 ++ src/helpers/radiolib/CustomLR1110Wrapper.h | 10 ++ src/helpers/radiolib/CustomSTM32WLxWrapper.h | 9 ++ src/helpers/radiolib/CustomSX1262Wrapper.h | 9 ++ src/helpers/radiolib/CustomSX1268Wrapper.h | 9 ++ src/helpers/radiolib/CustomSX1276Wrapper.h | 9 ++ 17 files changed, 317 insertions(+), 6 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index bf77872f84..f9dcc16ac2 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -556,6 +556,24 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change adaptive coding rate for direct retry packets +**Usage:** +- `get direct.retry.cr` +- `set direct.retry.cr ,,` +- `set direct.retry.cr ,,,` + +**Parameters:** +- `cr4_min`: SNR in dB where retry packets use `CR4` +- `cr5_min`: SNR in dB where retry packets use `CR5` +- `cr8_max`: SNR in dB where retry packets use `CR8` +- `low`: optional repeated low boundary; both low values must match + +**Default:** `10.0,7.5,2.5,2.5` + +**Note:** DM retry packets with a recent repeater table entry use that entry's SNR to pick a local transmit coding rate. With the default, SNR `10.0 dB` and up uses `CR4`, SNR `7.5 dB` and up uses `CR5`, SNR `2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. `CR6` is never selected. The shorter form `set direct.retry.cr 10.0,7.5,2.5` is equivalent to `set direct.retry.cr 10.0,7.5,2.5,2.5`. + +--- + #### View or change the SNR margin used for direct retry eligibility **Usage:** - `get direct.retry.margin` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index f9a961b9f4..24e5c81d15 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -846,6 +846,55 @@ bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_ho return true; } +uint8_t MyMesh::getDirectRetryCodingRateForSNR(int8_t snr_x4) const { + if (snr_x4 >= _prefs.direct_retry_cr4_snr_x4) { + return 4; + } + if (snr_x4 >= _prefs.direct_retry_cr5_snr_x4) { + return 5; + } + if (snr_x4 <= _prefs.direct_retry_cr8_snr_x4) { + return 8; + } + return 7; +} +void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) { + (void) original; + (void) retry_attempt; + + if (retry == NULL || !retry->isRouteDirect()) { + return; + } + + switch (retry->getPayloadType()) { + case PAYLOAD_TYPE_ACK: + case PAYLOAD_TYPE_PATH: + case PAYLOAD_TYPE_REQ: + case PAYLOAD_TYPE_RESPONSE: + case PAYLOAD_TYPE_TXT_MSG: + case PAYLOAD_TYPE_ANON_REQ: + case PAYLOAD_TYPE_MULTIPART: + break; + default: + return; + } + + uint8_t prefix[MAX_ROUTE_HASH_BYTES]; + uint8_t prefix_len = 0; + if (!extractDirectRetryPrefix(retry, prefix, prefix_len)) { + return; + } + + const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(prefix, prefix_len); + if (recent == NULL) { + return; + } + + uint8_t retry_cr = getDirectRetryCodingRateForSNR(recent->snr_x4); + if (retry_cr >= 4 && retry_cr <= 8 && retry_cr != active_cr) { + retry->tx_cr = retry_cr; + } +} uint8_t MyMesh::getDirectRetryPreset() const { if (_prefs.direct_retry_preset <= DIRECT_RETRY_PRESET_MOBILE) { return _prefs.direct_retry_preset; @@ -1243,6 +1292,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS; _prefs.direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS; _prefs.direct_retry_preset = DIRECT_RETRY_PRESET_ROOFTOP; + _prefs.direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT; + _prefs.direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; + _prefs.direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; _prefs.node_lon = ADVERT_LON; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 1a2f505c5e..a3a67676e9 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -124,6 +124,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { bool extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const; int8_t getDirectRetryMinSNRX4() const; + uint8_t getDirectRetryCodingRateForSNR(int8_t snr_x4) const; uint8_t getDirectRetryPreset() const; uint8_t getDirectRetryConfiguredMaxAttempts() const; uint32_t getDirectRetryAttemptStepMillis() const; @@ -154,7 +155,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t getRetransmitDelay(const mesh::Packet* packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; + uint8_t getDefaultTxCodingRate() const override { return active_cr; } bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override; + void configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) override; uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override; uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override; uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index cccbd36c79..4eb7eaf1db 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -52,6 +52,13 @@ void Dispatcher::updateTxBudget() { } } +void Dispatcher::restoreOutboundCodingRate() { + if (outbound_restore_cr != 0) { + _radio->setCodingRate(outbound_restore_cr); + outbound_restore_cr = 0; + } +} + int Dispatcher::calcRxDelay(float score, uint32_t air_time) const { return (int) ((pow(10, 0.85f - score) - 1.0) * air_time); } @@ -105,6 +112,7 @@ void Dispatcher::loop() { } _radio->onSendFinished(); + restoreOutboundCodingRate(); logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); onSendComplete(outbound); if (outbound->isRouteFlood()) { @@ -118,6 +126,7 @@ void Dispatcher::loop() { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime()); _radio->onSendFinished(); + restoreOutboundCodingRate(); logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); onSendFail(outbound); @@ -150,6 +159,7 @@ void Dispatcher::loop() { bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) { int i = 0; + pkt->tx_cr = 0; pkt->header = raw[i++]; if (pkt->getPayloadVer() > PAYLOAD_VER_1) { MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): unsupported packet version", getLogDateTime()); @@ -326,11 +336,23 @@ void Dispatcher::checkSend() { memcpy(&raw[len], outbound->payload, outbound->payload_len); len += outbound->payload_len; uint32_t max_airtime = _radio->getEstAirtimeFor(len)*3/2; + outbound_restore_cr = 0; + uint8_t default_cr = getDefaultTxCodingRate(); + if (outbound->tx_cr >= 4 && outbound->tx_cr <= 8 && default_cr >= 4 && default_cr <= 8 + && outbound->tx_cr != default_cr) { + if (_radio->setCodingRate(outbound->tx_cr)) { + outbound_restore_cr = default_cr; + max_airtime = _radio->getEstAirtimeFor(len)*3/2; + } else { + MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): WARN: failed to set packet CR%d", getLogDateTime(), (uint32_t)outbound->tx_cr); + } + } outbound_start = _ms->getMillis(); bool success = _radio->startSendRaw(raw, len); if (!success) { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): ERROR: send start failed!", getLogDateTime()); + restoreOutboundCodingRate(); logTxFail(outbound, outbound->getRawLength()); releasePacket(outbound); // return to pool @@ -361,6 +383,7 @@ Packet* Dispatcher::obtainNewPacket() { } else { pkt->payload_len = pkt->path_len = 0; pkt->_snr = 0; + pkt->tx_cr = 0; } return pkt; } diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 90ee5cdbea..2a910d8fec 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -46,6 +46,12 @@ class Radio { */ virtual bool startSendRaw(const uint8_t* bytes, int len) = 0; + /** + * \brief Sets LoRa coding rate for subsequent transmits/receives. + * \returns true if the radio accepted the coding rate. + */ + virtual bool setCodingRate(uint8_t cr) { return false; } + /** * \returns true if the previous 'startSendRaw()' completed successfully. */ @@ -116,6 +122,7 @@ typedef uint32_t DispatcherAction; class Dispatcher { Packet* outbound; // current outbound packet unsigned long outbound_expiry, outbound_start, total_air_time, rx_air_time; + uint8_t outbound_restore_cr; unsigned long next_tx_time; unsigned long cad_busy_start; unsigned long radio_nonrx_start; @@ -128,6 +135,7 @@ class Dispatcher { unsigned long duty_cycle_window_ms; void processRecvPacket(Packet* pkt); + void restoreOutboundCodingRate(); void updateTxBudget(); protected: @@ -140,6 +148,7 @@ class Dispatcher { : _radio(&radio), _ms(&ms), _mgr(&mgr) { outbound = NULL; + outbound_restore_cr = 0; total_air_time = rx_air_time = 0; next_tx_time = ms.getMillis(); cad_busy_start = 0; @@ -167,6 +176,7 @@ class Dispatcher { virtual int calcRxDelay(float score, uint32_t air_time) const; virtual uint32_t getCADFailRetryDelay() const; virtual uint32_t getCADFailMaxDuration() const; + virtual uint8_t getDefaultTxCodingRate() const { return 0; } virtual int getInterferenceThreshold() const { return 0; } // disabled by default virtual int getAGCResetInterval() const { return 0; } // disabled by default virtual unsigned long getDutyCycleWindowMs() const { return 3600000; } diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 42bcf5b10e..9228c88c0e 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -626,6 +626,9 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { } *retry = *packet; + retry->tx_cr = 0; + uint8_t retry_attempt = _direct_retries[i].retry_attempts_sent + 1; + configureDirectRetryPacket(retry, packet, retry_attempt); uint32_t retry_delay = getDirectRetryAttemptDelay(packet, _direct_retries[i].retry_attempts_sent); sendPacket(retry, _direct_retries[i].priority, retry_delay); if (isDirectRetryQueued(retry)) { @@ -633,10 +636,10 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { _direct_retries[i].retry_delay = retry_delay; _direct_retries[i].retry_at = futureMillis(retry_delay); _direct_retries[i].waiting_final_echo = false; - onDirectRetryEvent("queued", retry, retry_delay, _direct_retries[i].retry_attempts_sent + 1); + onDirectRetryEvent("queued", retry, retry_delay, retry_attempt); } else { - onDirectRetryEvent("dropped_queue_full", retry, retry_delay, _direct_retries[i].retry_attempts_sent + 1); - onDirectRetryEvent("failure", retry, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1); + onDirectRetryEvent("dropped_queue_full", retry, retry_delay, retry_attempt); + onDirectRetryEvent("failure", retry, elapsed_millis, retry_attempt); clearDirectRetrySlot(i); } } @@ -657,6 +660,8 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { } *retry = *packet; + retry->tx_cr = 0; + configureDirectRetryPacket(retry, packet, 1); // Start the echo wait only after the initial direct transmission actually completed. sendPacket(retry, _direct_retries[i].priority, _direct_retries[i].retry_delay); diff --git a/src/Mesh.h b/src/Mesh.h index 91aec9a669..f2ce453874 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -130,6 +130,11 @@ class Mesh : public Dispatcher { */ virtual void onDirectRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { } + /** + * \brief Optional hook to set local-only transmit options on a retry packet before it is queued. + */ + virtual void configureDirectRetryPacket(Packet* retry, const Packet* original, uint8_t retry_attempt) { } + /** * \brief Perform search of local DB of peers/contacts. * \returns Number of peers with matching hash diff --git a/src/Packet.cpp b/src/Packet.cpp index aad3e2f48e..3aab6349c5 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -8,6 +8,7 @@ Packet::Packet() { header = 0; path_len = 0; payload_len = 0; + tx_cr = 0; } bool Packet::isValidPathLen(uint8_t path_len) { @@ -64,6 +65,7 @@ uint8_t Packet::writeTo(uint8_t dest[]) const { bool Packet::readFrom(const uint8_t src[], uint8_t len) { uint8_t i = 0; + tx_cr = 0; header = src[i++]; if (hasTransportCodes()) { memcpy(&transport_codes[0], &src[i], 2); i += 2; @@ -84,4 +86,4 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) { return true; // success } -} \ No newline at end of file +} diff --git a/src/Packet.h b/src/Packet.h index 0886a06c4e..2943c03770 100644 --- a/src/Packet.h +++ b/src/Packet.h @@ -49,6 +49,7 @@ class Packet { uint8_t path[MAX_PATH_SIZE]; uint8_t payload[MAX_PACKET_PAYLOAD]; int8_t _snr; + uint8_t tx_cr; // volatile local-only TX coding-rate override; not serialized /** * \brief calculate the hash of payload + type diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index dc206516f1..42b41f2940 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -85,6 +85,38 @@ static uint8_t directRetryEffectiveMarginX4(const NodePrefs* prefs) { return constrain(prefs->direct_retry_snr_margin_db, (uint8_t)0, (uint8_t)DIRECT_RETRY_SNR_MARGIN_X4_MAX); } +static float directRetryCrX4ToDb(int8_t snr_x4) { + return ((float)snr_x4) / 4.0f; +} + +static void setDirectRetryCrDefaults(NodePrefs* prefs) { + prefs->direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT; + prefs->direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; + prefs->direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; +} + +static bool directRetryCrThresholdsAreValid(int8_t cr4_snr_x4, int8_t cr5_snr_x4, int8_t cr8_snr_x4) { + return (cr4_snr_x4 != 0 || cr5_snr_x4 != 0 || cr8_snr_x4 != 0) + && cr4_snr_x4 >= cr5_snr_x4 + && cr5_snr_x4 >= cr8_snr_x4; +} + +static void sanitizeDirectRetryCrThresholds(NodePrefs* prefs) { + if (!directRetryCrThresholdsAreValid(prefs->direct_retry_cr4_snr_x4, + prefs->direct_retry_cr5_snr_x4, + prefs->direct_retry_cr8_snr_x4)) { + setDirectRetryCrDefaults(prefs); + } +} + +static void formatDirectRetryCrThresholds(const NodePrefs* prefs, char* reply) { + char cr4[12], cr5[12], cr8[12]; + strcpy(cr4, StrHelper::ftoa(directRetryCrX4ToDb(prefs->direct_retry_cr4_snr_x4))); + strcpy(cr5, StrHelper::ftoa(directRetryCrX4ToDb(prefs->direct_retry_cr5_snr_x4))); + strcpy(cr8, StrHelper::ftoa(directRetryCrX4ToDb(prefs->direct_retry_cr8_snr_x4))); + sprintf(reply, "> %s,%s,%s,%s", cr4, cr5, cr8, cr8); +} + static uint16_t directRetryPresetStepDefault(uint8_t preset) { switch (directRetryPresetOrDefault(preset)) { case DIRECT_RETRY_PRESET_INFRA: @@ -142,6 +174,63 @@ static bool parseDirectRetryPreset(const char* value, uint8_t& preset) { return false; } +static bool parseDirectRetryCrDb(const char* value, int8_t& snr_x4) { + if (value == NULL) { + return false; + } + + char* end = NULL; + float snr_db = strtof(value, &end); + while (end != NULL && *end == ' ') end++; + if (end == value || (end != NULL && *end != 0)) { + return false; + } + + int32_t scaled_x4 = (int32_t)((snr_db * 4.0f) + (snr_db >= 0.0f ? 0.5f : -0.5f)); + if (scaled_x4 < DIRECT_RETRY_CR_SNR_X4_MIN || scaled_x4 > DIRECT_RETRY_CR_SNR_X4_MAX) { + return false; + } + snr_x4 = (int8_t)scaled_x4; + return true; +} + +static bool parseDirectRetryCrThresholds(char* value, NodePrefs* prefs) { + if (value == NULL || prefs == NULL) { + return false; + } + + const char* parts[4]; + int num = mesh::Utils::parseTextParts(value, parts, 4); + if (num != 3 && num != 4) { + return false; + } + + int8_t cr4_snr_x4; + int8_t cr5_snr_x4; + int8_t cr8_snr_x4; + if (!parseDirectRetryCrDb(parts[0], cr4_snr_x4) + || !parseDirectRetryCrDb(parts[1], cr5_snr_x4) + || !parseDirectRetryCrDb(parts[num == 4 ? 3 : 2], cr8_snr_x4)) { + return false; + } + + if (num == 4) { + int8_t repeated_low_snr_x4; + if (!parseDirectRetryCrDb(parts[2], repeated_low_snr_x4) || repeated_low_snr_x4 != cr8_snr_x4) { + return false; + } + } + + if (!directRetryCrThresholdsAreValid(cr4_snr_x4, cr5_snr_x4, cr8_snr_x4)) { + return false; + } + + prefs->direct_retry_cr4_snr_x4 = cr4_snr_x4; + prefs->direct_retry_cr5_snr_x4 = cr5_snr_x4; + prefs->direct_retry_cr8_snr_x4 = cr8_snr_x4; + return true; +} + static bool isValidName(const char *n) { while (*n) { if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false; @@ -157,6 +246,8 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) { loadPrefsInt(fs, "/node_prefs"); savePrefs(fs); // save to new filename fs->remove("/node_prefs"); // remove old + } else { + setDirectRetryCrDefaults(_prefs); } } @@ -227,6 +318,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->direct_retry_preset, sizeof(_prefs->direct_retry_preset)); // 297 size_t retry_step_read = file.read((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 298 + size_t retry_cr_read = 0; + retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, + sizeof(_prefs->direct_retry_cr4_snr_x4)); // 300 + retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, + sizeof(_prefs->direct_retry_cr5_snr_x4)); // 301 + retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, + sizeof(_prefs->direct_retry_cr8_snr_x4)); // 302 // PowerSaving-only prefs stored radio_fem_rxgain at 291, before direct retry timing existed. if (radio_fem_rxgain_read != sizeof(_prefs->radio_fem_rxgain) && legacy_retry_attempts_read == sizeof(legacy_retry_attempts_or_radio_fem_rxgain) @@ -234,7 +332,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1)) { _prefs->radio_fem_rxgain = constrain(legacy_retry_attempts_or_radio_fem_rxgain, 0, 1); } - // next: 298 + // next: 303 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -288,6 +386,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { } else { _prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); } + if (retry_cr_read != sizeof(_prefs->direct_retry_cr4_snr_x4) + + sizeof(_prefs->direct_retry_cr5_snr_x4) + + sizeof(_prefs->direct_retry_cr8_snr_x4)) { + setDirectRetryCrDefaults(_prefs); + } else { + sanitizeDirectRetryCrThresholds(_prefs); + } file.close(); } @@ -360,7 +465,10 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 296 file.write((uint8_t *)&_prefs->direct_retry_preset, sizeof(_prefs->direct_retry_preset)); // 297 file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 298 - // next: 300 + file.write((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 300 + file.write((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 301 + file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 302 + // next: 303 file.close(); } @@ -532,6 +640,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re sprintf(reply, "> %d", (uint32_t)directRetryEffectiveBaseMs(_prefs)); } else if (memcmp(config, "direct.retry.step", 17) == 0) { sprintf(reply, "> %d", (uint32_t)directRetryEffectiveStepMs(_prefs)); + } else if (memcmp(config, "direct.retry.cr", 15) == 0) { + formatDirectRetryCrThresholds(_prefs, reply); } else if (memcmp(config, "owner.info", 10) == 0) { *reply++ = '>'; *reply++ = ' '; @@ -838,6 +948,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); } + } else if (memcmp(config, "direct.retry.cr ", 16) == 0) { + StrHelper::strncpy(tmp, &config[16], sizeof(tmp)); + if (parseDirectRetryCrThresholds(tmp, _prefs)) { + savePrefs(); + formatDirectRetryCrThresholds(_prefs, reply); + } else { + strcpy(reply, "Error, expected cr4,cr5,cr8 or cr4,cr5,low,low"); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; @@ -1392,6 +1510,14 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); } + } else if (memcmp(config, "direct.retry.cr ", 16) == 0) { + StrHelper::strncpy(tmp, &config[16], sizeof(tmp)); + if (parseDirectRetryCrThresholds(tmp, _prefs)) { + savePrefs(); + formatDirectRetryCrThresholds(_prefs, reply); + } else { + strcpy(reply, "Error, expected cr4,cr5,cr8 or cr4,cr5,low,low"); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; @@ -1581,6 +1707,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %d", (uint32_t)directRetryEffectiveBaseMs(_prefs)); } else if (memcmp(config, "direct.retry.step", 17) == 0) { sprintf(reply, "> %d", (uint32_t)directRetryEffectiveStepMs(_prefs)); + } else if (memcmp(config, "direct.retry.cr", 15) == 0) { + formatDirectRetryCrThresholds(_prefs, reply); } else if (memcmp(config, "owner.info", 10) == 0) { *reply++ = '>'; *reply++ = ' '; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index ea30777a2d..43e8e6d7fb 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -38,6 +38,12 @@ #define DIRECT_RETRY_MOBILE_STEP_MS 50 #define DIRECT_RETRY_MOBILE_MARGIN_X4 0 +#define DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT 40 // 10.0 dB and up => CR4 +#define DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT 30 // 7.5 dB and up => CR5 +#define DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT 10 // 2.5 dB and down => CR8 +#define DIRECT_RETRY_CR_SNR_X4_MIN -128 +#define DIRECT_RETRY_CR_SNR_X4_MAX 127 + struct NodePrefs { // persisted to file float airtime_factor; char node_name[32]; @@ -88,6 +94,9 @@ struct NodePrefs { // persisted to file uint8_t direct_retry_timing_magic[2]; uint8_t direct_retry_preset; uint16_t direct_retry_step_ms; + int8_t direct_retry_cr4_snr_x4; + int8_t direct_retry_cr5_snr_x4; + int8_t direct_retry_cr8_snr_x4; }; class CommonCLICallbacks { diff --git a/src/helpers/radiolib/CustomLLCC68Wrapper.h b/src/helpers/radiolib/CustomLLCC68Wrapper.h index fc0975cf65..1b1ddcaa6f 100644 --- a/src/helpers/radiolib/CustomLLCC68Wrapper.h +++ b/src/helpers/radiolib/CustomLLCC68Wrapper.h @@ -20,6 +20,15 @@ class CustomLLCC68Wrapper : public RadioLibWrapper { int sf = ((CustomLLCC68 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); } + bool setCodingRate(uint8_t cr) override { + idle(); + int err = ((CustomLLCC68 *)_radio)->setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) { + MESH_DEBUG_PRINTLN("CustomLLCC68Wrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err); + return false; + } + return true; + } void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 42d364408c..b07e561dba 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -25,6 +25,16 @@ class CustomLR1110Wrapper : public RadioLibWrapper { float getLastRSSI() const override { return ((CustomLR1110 *)_radio)->getRSSI(); } float getLastSNR() const override { return ((CustomLR1110 *)_radio)->getSNR(); } + bool setCodingRate(uint8_t cr) override { + idle(); + int err = ((CustomLR1110 *)_radio)->setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) { + MESH_DEBUG_PRINTLN("CustomLR1110Wrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err); + return false; + } + return true; + } + void setRxBoostedGainMode(bool en) override { ((CustomLR1110 *)_radio)->setRxBoostedGainMode(en); } diff --git a/src/helpers/radiolib/CustomSTM32WLxWrapper.h b/src/helpers/radiolib/CustomSTM32WLxWrapper.h index e3e5202949..d0aa2dae9a 100644 --- a/src/helpers/radiolib/CustomSTM32WLxWrapper.h +++ b/src/helpers/radiolib/CustomSTM32WLxWrapper.h @@ -21,6 +21,15 @@ class CustomSTM32WLxWrapper : public RadioLibWrapper { int sf = ((CustomSTM32WLx *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); } + bool setCodingRate(uint8_t cr) override { + idle(); + int err = ((CustomSTM32WLx *)_radio)->setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) { + MESH_DEBUG_PRINTLN("CustomSTM32WLxWrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err); + return false; + } + return true; + } void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } }; diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 6499deb296..72f6ba3881 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -24,6 +24,15 @@ class CustomSX1262Wrapper : public RadioLibWrapper { int sf = ((CustomSX1262 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); } + bool setCodingRate(uint8_t cr) override { + idle(); + int err = ((CustomSX1262 *)_radio)->setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) { + MESH_DEBUG_PRINTLN("CustomSX1262Wrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err); + return false; + } + return true; + } virtual void powerOff() override { ((CustomSX1262 *)_radio)->sleep(false); } diff --git a/src/helpers/radiolib/CustomSX1268Wrapper.h b/src/helpers/radiolib/CustomSX1268Wrapper.h index 54c37ee8aa..50dfa9c2c3 100644 --- a/src/helpers/radiolib/CustomSX1268Wrapper.h +++ b/src/helpers/radiolib/CustomSX1268Wrapper.h @@ -24,6 +24,15 @@ class CustomSX1268Wrapper : public RadioLibWrapper { int sf = ((CustomSX1268 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); } + bool setCodingRate(uint8_t cr) override { + idle(); + int err = ((CustomSX1268 *)_radio)->setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) { + MESH_DEBUG_PRINTLN("CustomSX1268Wrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err); + return false; + } + return true; + } void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } diff --git a/src/helpers/radiolib/CustomSX1276Wrapper.h b/src/helpers/radiolib/CustomSX1276Wrapper.h index 5cde72f750..dd976306d9 100644 --- a/src/helpers/radiolib/CustomSX1276Wrapper.h +++ b/src/helpers/radiolib/CustomSX1276Wrapper.h @@ -23,4 +23,13 @@ class CustomSX1276Wrapper : public RadioLibWrapper { int sf = ((CustomSX1276 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); } + bool setCodingRate(uint8_t cr) override { + idle(); + int err = ((CustomSX1276 *)_radio)->setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) { + MESH_DEBUG_PRINTLN("CustomSX1276Wrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err); + return false; + } + return true; + } }; From 7d237f873001b1f08775ca5da80d7a284ab20e8f Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 8 May 2026 16:57:03 -0700 Subject: [PATCH 38/94] Update keymind retry controls --- docs/cli_commands.md | 10 +-- docs/halo_keymind_settings.md | 11 +++- examples/simple_repeater/MyMesh.cpp | 37 ++++++++--- src/Mesh.cpp | 5 +- src/helpers/ArduinoSerialInterface.cpp | 4 ++ src/helpers/ArduinoSerialInterface.h | 3 +- src/helpers/CommonCLI.cpp | 86 ++++++++++++++++++-------- src/helpers/CommonCLI.h | 2 + 8 files changed, 114 insertions(+), 44 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 505a82059a..18e8016370 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -559,18 +559,18 @@ This document provides an overview of CLI commands that can be sent to MeshCore #### View or change adaptive coding rate for direct retry packets **Usage:** - `get direct.retry.cr` -- `set direct.retry.cr ,,` -- `set direct.retry.cr ,,,` +- `set direct.retry.cr ,,,` +- `set direct.retry.cr off` **Parameters:** - `cr4_min`: SNR in dB where retry packets use `CR4` - `cr5_min`: SNR in dB where retry packets use `CR5` +- `cr7_min`: SNR in dB where retry packets use `CR7` - `cr8_max`: SNR in dB where retry packets use `CR8` -- `low`: optional repeated low boundary; both low values must match **Default:** `10.0,7.5,2.5,2.5` -**Note:** DM retry packets with a recent repeater table entry use that entry's SNR to pick a local transmit coding rate. With the default, SNR `10.0 dB` and up uses `CR4`, SNR `7.5 dB` and up uses `CR5`, SNR `2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. `CR6` is never selected. The shorter form `set direct.retry.cr 10.0,7.5,2.5` is equivalent to `set direct.retry.cr 10.0,7.5,2.5,2.5`. +**Note:** DM retry packets use the next-hop SNR from a recent repeater table entry to pick a local transmit coding rate; if no recent entry is available, retry packets use `CR5`. With the default, SNR `10.0 dB` and up uses `CR4`, SNR `7.5 dB` and up uses `CR5`, SNR `2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. `CR6` is never selected. Use `set direct.retry.cr off` to disable adaptive coding-rate overrides. If adaptive selection chooses `CR4`, retries after the third attempt use `CR5`. --- @@ -778,7 +778,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `set flood.retry.count ` **Parameters:** -- `value`: Maximum retry attempts after initial flood TX (`0`-`3`) +- `value`: Maximum retry attempts after initial flood TX (`0`-`15`) **Default:** `3` for `rooftop` and `mobile`, `1` for `infra` diff --git a/docs/halo_keymind_settings.md b/docs/halo_keymind_settings.md index 908c390ca3..1be3ddb0f8 100644 --- a/docs/halo_keymind_settings.md +++ b/docs/halo_keymind_settings.md @@ -111,6 +111,15 @@ Direct retry applies to direct-routed packets. A queued resend is canceled when | `direct.retry.count` | Maximum direct retry attempts after initial TX. | `get direct.retry.count`, `set direct.retry.count <1-15>` | `set direct.retry.count 15` | | `direct.retry.base` | Base wait in milliseconds before retry. | `get direct.retry.base`, `set direct.retry.base <10-5000>` | `set direct.retry.base 175` | | `direct.retry.step` | Milliseconds added per retry attempt. | `get direct.retry.step`, `set direct.retry.step <0-5000>` | `set direct.retry.step 100` | +| `direct.retry.cr` | Adaptive coding-rate thresholds for direct retry packets. Uses `CR4`, `CR5`, `CR7`, or `CR8`; `CR6` is never selected. | `get direct.retry.cr`, `set direct.retry.cr ,,,`, `set direct.retry.cr off` | `set direct.retry.cr 10.0,7.5,2.5,0` | + +The default adaptive coding-rate profile is `10.0,7.5,2.5,2.5`. +SNR `10.0 dB` and up uses `CR4`, `7.5 dB` and up uses `CR5`, +`2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. If no +recent repeater table entry is available, retry packets use `CR5`. Use +`set direct.retry.cr off` to disable adaptive coding-rate overrides. If +adaptive selection chooses `CR4`, retries after the third attempt use +`CR5`. Preset details: @@ -141,7 +150,7 @@ Flood retry applies to flood-routed packets. A queued retry is canceled when a q | Setting | What it does | How to use | Example | | --- | --- | --- | --- | -| `flood.retry.count` | Maximum flood retry attempts after initial TX. `0` disables flood retry. | `get flood.retry.count`, `set flood.retry.count <0-3>` | `set flood.retry.count 3` | +| `flood.retry.count` | Maximum flood retry attempts after initial TX. `0` disables flood retry. | `get flood.retry.count`, `set flood.retry.count <0-15>` | `set flood.retry.count 7` | | `flood.retry.path` | Maximum path hash count eligible for flood retry, or `off` to disable the gate. | `get flood.retry.path`, `set flood.retry.path <0-63/off>` | `set flood.retry.path 1` | | `flood.retry.advert` | Allows or blocks retry for node advert packets (`type=4`). Default is `off`. | `get flood.retry.advert`, `set flood.retry.advert on/off` | `set flood.retry.advert off` | | `flood.retry.prefixes` | Target prefixes. If set, only matching downstream echoes cancel a retry. | `get flood.retry.prefixes`, `set flood.retry.prefixes ` | `set flood.retry.prefixes BEEBB0,425E5C` | diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index b127db30b2..d8e72cde80 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -744,7 +744,8 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u time_label = "echo_ms"; } - MESH_DEBUG_PRINTLN("%s direct retry %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%s, next_hop=%s, pkt=%s, tbl=%s, %s=%lu)", + uint8_t log_cr = (packet->tx_cr >= 4 && packet->tx_cr <= 8) ? packet->tx_cr : active_cr; + MESH_DEBUG_PRINTLN("%s direct retry %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%s, next_hop=%s, pkt=%s, tbl=%s, cr=%u, %s=%lu)", getLogDateTime(), event, (unsigned int)retry_attempt, @@ -755,6 +756,7 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u next_hop, snr_pkt_text, snr_table_text, + (unsigned int)log_cr, time_label, (unsigned long)delay_millis); @@ -762,7 +764,7 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u File f = openAppend(PACKET_LOG_FILE); if (f) { f.print(getLogDateTime()); - f.printf(": DIRECT RETRY %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%s, next_hop=%s, pkt=%s, tbl=%s, %s=%lu)\n", + f.printf(": DIRECT RETRY %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%s, next_hop=%s, pkt=%s, tbl=%s, cr=%u, %s=%lu)\n", event, (unsigned int)retry_attempt, (uint32_t)packet->getPayloadType(), @@ -772,6 +774,7 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u next_hop, snr_pkt_text, snr_table_text, + (unsigned int)log_cr, time_label, (unsigned long)delay_millis); f.close(); @@ -847,6 +850,12 @@ bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_ho return true; } uint8_t MyMesh::getDirectRetryCodingRateForSNR(int8_t snr_x4) const { + if (_prefs.direct_retry_cr4_snr_x4 == 0 + && _prefs.direct_retry_cr5_snr_x4 == 0 + && _prefs.direct_retry_cr7_snr_x4 == 0 + && _prefs.direct_retry_cr8_snr_x4 == 0) { + return 0; + } if (snr_x4 >= _prefs.direct_retry_cr4_snr_x4) { return 4; } @@ -856,11 +865,13 @@ uint8_t MyMesh::getDirectRetryCodingRateForSNR(int8_t snr_x4) const { if (snr_x4 <= _prefs.direct_retry_cr8_snr_x4) { return 8; } + if (snr_x4 >= _prefs.direct_retry_cr7_snr_x4) { + return 7; + } return 7; } void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) { (void) original; - (void) retry_attempt; if (retry == NULL || !retry->isRouteDirect()) { return; @@ -873,6 +884,7 @@ void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* case PAYLOAD_TYPE_RESPONSE: case PAYLOAD_TYPE_TXT_MSG: case PAYLOAD_TYPE_ANON_REQ: + case PAYLOAD_TYPE_TRACE: case PAYLOAD_TYPE_MULTIPART: break; default: @@ -885,13 +897,19 @@ void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* return; } - const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(prefix, prefix_len); - if (recent == NULL) { + if (_prefs.direct_retry_cr4_snr_x4 == 0 + && _prefs.direct_retry_cr5_snr_x4 == 0 + && _prefs.direct_retry_cr7_snr_x4 == 0 + && _prefs.direct_retry_cr8_snr_x4 == 0) { return; } - uint8_t retry_cr = getDirectRetryCodingRateForSNR(recent->snr_x4); - if (retry_cr >= 4 && retry_cr <= 8 && retry_cr != active_cr) { + const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(prefix, prefix_len); + uint8_t retry_cr = (recent != NULL) ? getDirectRetryCodingRateForSNR(recent->snr_x4) : 5; + if (retry_cr == 4 && retry_attempt > 3) { + retry_cr = 5; + } + if (retry_cr >= 4 && retry_cr <= 8 && retry_cr != 6 && retry_cr != active_cr) { retry->tx_cr = retry_cr; } } @@ -1145,7 +1163,7 @@ uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { return base_wait_millis + scaled_wait_millis; } bool MyMesh::allowFloodRetry(const mesh::Packet* packet) const { - if (_prefs.disable_fwd || constrain(_prefs.flood_retry_attempts, (uint8_t)0, (uint8_t)3) == 0) { + if (_prefs.disable_fwd || constrain(_prefs.flood_retry_attempts, (uint8_t)0, (uint8_t)15) == 0) { return false; } if (packet != NULL && packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && !_prefs.flood_retry_advert_enabled) { @@ -1401,7 +1419,7 @@ uint8_t MyMesh::getFloodRetryMaxAttempts(const mesh::Packet* packet) const { if (_prefs.disable_fwd) { return 0; } - return constrain(_prefs.flood_retry_attempts, (uint8_t)0, (uint8_t)3); + return constrain(_prefs.flood_retry_attempts, (uint8_t)0, (uint8_t)15); } bool MyMesh::isFloodRetryEchoTarget(const mesh::Packet* packet, uint8_t progress_marker) const { if (packet == NULL || !packet->isRouteFlood()) { @@ -1809,6 +1827,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_retry_advert_enabled = 0; _prefs.direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT; _prefs.direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; + _prefs.direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; _prefs.direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 730d9f499d..747ab36df7 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -6,6 +6,7 @@ namespace mesh { static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 15; static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; static const uint8_t FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT = 3; +static const uint8_t FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) { uint8_t code = flags & 0x03; @@ -966,8 +967,8 @@ void Mesh::armFloodRetryOnSendComplete(const Packet* packet) { uint8_t max_attempts = getFloodRetryMaxAttempts(packet); if (max_attempts < 1) { max_attempts = 1; - } else if (max_attempts > FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT) { - max_attempts = FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT; + } else if (max_attempts > FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX) { + max_attempts = FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX; } if (_flood_retries[i].retry_attempts_sent >= max_attempts) { Packet* final_wait = obtainNewPacket(); diff --git a/src/helpers/ArduinoSerialInterface.cpp b/src/helpers/ArduinoSerialInterface.cpp index a01fa5866f..6b44397406 100644 --- a/src/helpers/ArduinoSerialInterface.cpp +++ b/src/helpers/ArduinoSerialInterface.cpp @@ -17,6 +17,10 @@ bool ArduinoSerialInterface::isConnected() const { return true; // no way of knowing, so assume yes } +bool ArduinoSerialInterface::isReadBusy() const { + return false; +} + bool ArduinoSerialInterface::isWriteBusy() const { return false; } diff --git a/src/helpers/ArduinoSerialInterface.h b/src/helpers/ArduinoSerialInterface.h index c4086353aa..4fa2b75d14 100644 --- a/src/helpers/ArduinoSerialInterface.h +++ b/src/helpers/ArduinoSerialInterface.h @@ -28,7 +28,8 @@ class ArduinoSerialInterface : public BaseSerialInterface { bool isConnected() const override; + bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; -}; \ No newline at end of file +}; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 6a383ce441..db56c5585c 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -33,7 +33,7 @@ #define FLOOD_RETRY_PREFS_MAGIC_0 0xF4 #define FLOOD_RETRY_PREFS_MAGIC_1 0x52 #define FLOOD_RETRY_COUNT_MIN 0 -#define FLOOD_RETRY_COUNT_MAX 3 +#define FLOOD_RETRY_COUNT_MAX 15 #define FLOOD_RETRY_ADVERT_DEFAULT 0 // Believe it or not, this std C function is busted on some platforms! @@ -121,29 +121,53 @@ static float directRetryCrX4ToDb(int8_t snr_x4) { static void setDirectRetryCrDefaults(NodePrefs* prefs) { prefs->direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT; prefs->direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; + prefs->direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; prefs->direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; } -static bool directRetryCrThresholdsAreValid(int8_t cr4_snr_x4, int8_t cr5_snr_x4, int8_t cr8_snr_x4) { - return (cr4_snr_x4 != 0 || cr5_snr_x4 != 0 || cr8_snr_x4 != 0) - && cr4_snr_x4 >= cr5_snr_x4 - && cr5_snr_x4 >= cr8_snr_x4; +static void setDirectRetryCrOff(NodePrefs* prefs) { + prefs->direct_retry_cr4_snr_x4 = 0; + prefs->direct_retry_cr5_snr_x4 = 0; + prefs->direct_retry_cr7_snr_x4 = 0; + prefs->direct_retry_cr8_snr_x4 = 0; +} + +static bool directRetryCrThresholdsAreOff(int8_t cr4_snr_x4, int8_t cr5_snr_x4, int8_t cr7_snr_x4, int8_t cr8_snr_x4) { + return cr4_snr_x4 == 0 && cr5_snr_x4 == 0 && cr7_snr_x4 == 0 && cr8_snr_x4 == 0; +} + +static bool directRetryCrThresholdsAreValid(int8_t cr4_snr_x4, int8_t cr5_snr_x4, int8_t cr7_snr_x4, int8_t cr8_snr_x4) { + return directRetryCrThresholdsAreOff(cr4_snr_x4, cr5_snr_x4, cr7_snr_x4, cr8_snr_x4) + || ((cr4_snr_x4 != 0 || cr5_snr_x4 != 0 || cr7_snr_x4 != 0 || cr8_snr_x4 != 0) + && cr4_snr_x4 >= cr5_snr_x4 + && cr5_snr_x4 >= cr7_snr_x4 + && cr7_snr_x4 >= cr8_snr_x4); } static void sanitizeDirectRetryCrThresholds(NodePrefs* prefs) { if (!directRetryCrThresholdsAreValid(prefs->direct_retry_cr4_snr_x4, prefs->direct_retry_cr5_snr_x4, + prefs->direct_retry_cr7_snr_x4, prefs->direct_retry_cr8_snr_x4)) { setDirectRetryCrDefaults(prefs); } } static void formatDirectRetryCrThresholds(const NodePrefs* prefs, char* reply) { - char cr4[12], cr5[12], cr8[12]; + if (directRetryCrThresholdsAreOff(prefs->direct_retry_cr4_snr_x4, + prefs->direct_retry_cr5_snr_x4, + prefs->direct_retry_cr7_snr_x4, + prefs->direct_retry_cr8_snr_x4)) { + strcpy(reply, "> off"); + return; + } + + char cr4[12], cr5[12], cr7[12], cr8[12]; strcpy(cr4, StrHelper::ftoa(directRetryCrX4ToDb(prefs->direct_retry_cr4_snr_x4))); strcpy(cr5, StrHelper::ftoa(directRetryCrX4ToDb(prefs->direct_retry_cr5_snr_x4))); + strcpy(cr7, StrHelper::ftoa(directRetryCrX4ToDb(prefs->direct_retry_cr7_snr_x4))); strcpy(cr8, StrHelper::ftoa(directRetryCrX4ToDb(prefs->direct_retry_cr8_snr_x4))); - sprintf(reply, "> %s,%s,%s,%s", cr4, cr5, cr8, cr8); + sprintf(reply, "> %s,%s,%s,%s", cr4, cr5, cr7, cr8); } static uint16_t retryPresetStepDefault(uint8_t preset) { @@ -388,35 +412,36 @@ static bool parseDirectRetryCrThresholds(char* value, NodePrefs* prefs) { if (value == NULL || prefs == NULL) { return false; } + if (strcmp(value, "off") == 0) { + setDirectRetryCrOff(prefs); + return true; + } - const char* parts[4]; - int num = mesh::Utils::parseTextParts(value, parts, 4); - if (num != 3 && num != 4) { + const char* parts[5]; + int num = mesh::Utils::parseTextParts(value, parts, 5); + if (num != 4) { return false; } int8_t cr4_snr_x4; int8_t cr5_snr_x4; + int8_t cr7_snr_x4; int8_t cr8_snr_x4; if (!parseDirectRetryCrDb(parts[0], cr4_snr_x4) || !parseDirectRetryCrDb(parts[1], cr5_snr_x4) - || !parseDirectRetryCrDb(parts[num == 4 ? 3 : 2], cr8_snr_x4)) { + || !parseDirectRetryCrDb(parts[2], cr7_snr_x4) + || !parseDirectRetryCrDb(parts[3], cr8_snr_x4)) { return false; } - if (num == 4) { - int8_t repeated_low_snr_x4; - if (!parseDirectRetryCrDb(parts[2], repeated_low_snr_x4) || repeated_low_snr_x4 != cr8_snr_x4) { - return false; - } - } - - if (!directRetryCrThresholdsAreValid(cr4_snr_x4, cr5_snr_x4, cr8_snr_x4)) { + if (directRetryCrThresholdsAreOff(cr4_snr_x4, cr5_snr_x4, cr7_snr_x4, cr8_snr_x4) + || !directRetryCrThresholdsAreValid(cr4_snr_x4, cr5_snr_x4, cr7_snr_x4, cr8_snr_x4)) { return false; } prefs->direct_retry_cr4_snr_x4 = cr4_snr_x4; prefs->direct_retry_cr5_snr_x4 = cr5_snr_x4; + prefs->direct_retry_cr7_snr_x4 = cr7_snr_x4; prefs->direct_retry_cr8_snr_x4 = cr8_snr_x4; return true; } @@ -527,8 +552,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { sizeof(_prefs->direct_retry_cr4_snr_x4)); // 660 retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 661 + retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, + sizeof(_prefs->direct_retry_cr7_snr_x4)); // 662 retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, - sizeof(_prefs->direct_retry_cr8_snr_x4)); // 662 + sizeof(_prefs->direct_retry_cr8_snr_x4)); // 663 // PowerSaving-only prefs stored radio_fem_rxgain at 291, before direct retry timing existed. if (radio_fem_rxgain_read != sizeof(_prefs->radio_fem_rxgain) && legacy_retry_attempts_read == sizeof(legacy_retry_attempts_or_radio_fem_rxgain) @@ -536,7 +563,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1)) { _prefs->radio_fem_rxgain = constrain(legacy_retry_attempts_or_radio_fem_rxgain, 0, 1); } - // next: 663 + // next: 664 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -614,8 +641,14 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->flood_retry_advert_enabled = constrain(_prefs->flood_retry_advert_enabled, 0, 1); } } - if (retry_cr_read != sizeof(_prefs->direct_retry_cr4_snr_x4) + if (retry_cr_read == sizeof(_prefs->direct_retry_cr4_snr_x4) + + sizeof(_prefs->direct_retry_cr5_snr_x4) + + sizeof(_prefs->direct_retry_cr7_snr_x4)) { + _prefs->direct_retry_cr8_snr_x4 = _prefs->direct_retry_cr7_snr_x4; + sanitizeDirectRetryCrThresholds(_prefs); + } else if (retry_cr_read != sizeof(_prefs->direct_retry_cr4_snr_x4) + sizeof(_prefs->direct_retry_cr5_snr_x4) + + sizeof(_prefs->direct_retry_cr7_snr_x4) + sizeof(_prefs->direct_retry_cr8_snr_x4)) { setDirectRetryCrDefaults(_prefs); } else { @@ -704,8 +737,9 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->flood_retry_advert_enabled, sizeof(_prefs->flood_retry_advert_enabled)); // 659 file.write((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 660 file.write((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 661 - file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 662 - // next: 663 + file.write((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 662 + file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 663 + // next: 664 file.close(); } @@ -1285,7 +1319,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re savePrefs(); formatDirectRetryCrThresholds(_prefs, reply); } else { - strcpy(reply, "Error, expected cr4,cr5,cr8 or cr4,cr5,low,low"); + strcpy(reply, "Error, expected off or cr4,cr5,cr7,cr8"); } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; @@ -1919,7 +1953,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep savePrefs(); formatDirectRetryCrThresholds(_prefs, reply); } else { - strcpy(reply, "Error, expected cr4,cr5,cr8 or cr4,cr5,low,low"); + strcpy(reply, "Error, expected off or cr4,cr5,cr7,cr8"); } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index ed57364977..104a7150c9 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -65,6 +65,7 @@ #define DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT 40 // 10.0 dB and up => CR4 #define DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT 30 // 7.5 dB and up => CR5 +#define DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT 10 // 2.5 dB and up => CR7 #define DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT 10 // 2.5 dB and down => CR8 #define DIRECT_RETRY_CR_SNR_X4_MIN -128 #define DIRECT_RETRY_CR_SNR_X4_MAX 127 @@ -129,6 +130,7 @@ struct NodePrefs { // persisted to file uint8_t flood_retry_advert_enabled; int8_t direct_retry_cr4_snr_x4; int8_t direct_retry_cr5_snr_x4; + int8_t direct_retry_cr7_snr_x4; int8_t direct_retry_cr8_snr_x4; }; From 9a8609097825980ea74e72409a345f6ea89cef44 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 8 May 2026 17:04:47 -0700 Subject: [PATCH 39/94] Update direct retry CR controls --- docs/cli_commands.md | 8 +-- examples/simple_repeater/MyMesh.cpp | 34 +++++++++--- src/helpers/CommonCLI.cpp | 84 ++++++++++++++++++++--------- src/helpers/CommonCLI.h | 2 + 4 files changed, 92 insertions(+), 36 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index f9dcc16ac2..ab6af5028b 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -559,18 +559,18 @@ This document provides an overview of CLI commands that can be sent to MeshCore #### View or change adaptive coding rate for direct retry packets **Usage:** - `get direct.retry.cr` -- `set direct.retry.cr ,,` -- `set direct.retry.cr ,,,` +- `set direct.retry.cr ,,,` +- `set direct.retry.cr off` **Parameters:** - `cr4_min`: SNR in dB where retry packets use `CR4` - `cr5_min`: SNR in dB where retry packets use `CR5` +- `cr7_min`: SNR in dB where retry packets use `CR7` - `cr8_max`: SNR in dB where retry packets use `CR8` -- `low`: optional repeated low boundary; both low values must match **Default:** `10.0,7.5,2.5,2.5` -**Note:** DM retry packets with a recent repeater table entry use that entry's SNR to pick a local transmit coding rate. With the default, SNR `10.0 dB` and up uses `CR4`, SNR `7.5 dB` and up uses `CR5`, SNR `2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. `CR6` is never selected. The shorter form `set direct.retry.cr 10.0,7.5,2.5` is equivalent to `set direct.retry.cr 10.0,7.5,2.5,2.5`. +**Note:** DM retry packets use the next-hop SNR from a recent repeater table entry to pick a local transmit coding rate; if no recent entry is available, retry packets use `CR5`. With the default, SNR `10.0 dB` and up uses `CR4`, SNR `7.5 dB` and up uses `CR5`, SNR `2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. `CR6` is never selected. Use `set direct.retry.cr off` to disable adaptive coding-rate overrides. If adaptive selection chooses `CR4`, retries after the third attempt use `CR5`. --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 24e5c81d15..89bd6020d6 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -744,7 +744,8 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u time_label = "echo_ms"; } - MESH_DEBUG_PRINTLN("%s direct retry %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%s, next_hop=%s, pkt=%s, tbl=%s, %s=%lu)", + uint8_t log_cr = (packet->tx_cr >= 4 && packet->tx_cr <= 8) ? packet->tx_cr : active_cr; + MESH_DEBUG_PRINTLN("%s direct retry %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%s, next_hop=%s, pkt=%s, tbl=%s, cr=%u, %s=%lu)", getLogDateTime(), event, (unsigned int)retry_attempt, @@ -755,6 +756,7 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u next_hop, snr_pkt_text, snr_table_text, + (unsigned int)log_cr, time_label, (unsigned long)delay_millis); @@ -762,7 +764,7 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u File f = openAppend(PACKET_LOG_FILE); if (f) { f.print(getLogDateTime()); - f.printf(": DIRECT RETRY %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%s, next_hop=%s, pkt=%s, tbl=%s, %s=%lu)\n", + f.printf(": DIRECT RETRY %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%s, next_hop=%s, pkt=%s, tbl=%s, cr=%u, %s=%lu)\n", event, (unsigned int)retry_attempt, (uint32_t)packet->getPayloadType(), @@ -772,6 +774,7 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u next_hop, snr_pkt_text, snr_table_text, + (unsigned int)log_cr, time_label, (unsigned long)delay_millis); f.close(); @@ -847,6 +850,12 @@ bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_ho return true; } uint8_t MyMesh::getDirectRetryCodingRateForSNR(int8_t snr_x4) const { + if (_prefs.direct_retry_cr4_snr_x4 == 0 + && _prefs.direct_retry_cr5_snr_x4 == 0 + && _prefs.direct_retry_cr7_snr_x4 == 0 + && _prefs.direct_retry_cr8_snr_x4 == 0) { + return 0; + } if (snr_x4 >= _prefs.direct_retry_cr4_snr_x4) { return 4; } @@ -856,11 +865,13 @@ uint8_t MyMesh::getDirectRetryCodingRateForSNR(int8_t snr_x4) const { if (snr_x4 <= _prefs.direct_retry_cr8_snr_x4) { return 8; } + if (snr_x4 >= _prefs.direct_retry_cr7_snr_x4) { + return 7; + } return 7; } void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) { (void) original; - (void) retry_attempt; if (retry == NULL || !retry->isRouteDirect()) { return; @@ -873,6 +884,7 @@ void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* case PAYLOAD_TYPE_RESPONSE: case PAYLOAD_TYPE_TXT_MSG: case PAYLOAD_TYPE_ANON_REQ: + case PAYLOAD_TYPE_TRACE: case PAYLOAD_TYPE_MULTIPART: break; default: @@ -885,13 +897,20 @@ void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* return; } - const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(prefix, prefix_len); - if (recent == NULL) { + if (_prefs.direct_retry_cr4_snr_x4 == 0 + && _prefs.direct_retry_cr5_snr_x4 == 0 + && _prefs.direct_retry_cr7_snr_x4 == 0 + && _prefs.direct_retry_cr8_snr_x4 == 0) { return; } - uint8_t retry_cr = getDirectRetryCodingRateForSNR(recent->snr_x4); - if (retry_cr >= 4 && retry_cr <= 8 && retry_cr != active_cr) { + const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(prefix, prefix_len); + uint8_t retry_cr = (recent != NULL) ? getDirectRetryCodingRateForSNR(recent->snr_x4) : 5; + if (retry_cr == 4 && retry_attempt > 3) { + retry_cr = 5; + } + + if (retry_cr >= 4 && retry_cr <= 8 && retry_cr != 6 && retry_cr != active_cr) { retry->tx_cr = retry_cr; } } @@ -1294,6 +1313,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.direct_retry_preset = DIRECT_RETRY_PRESET_ROOFTOP; _prefs.direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT; _prefs.direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; + _prefs.direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; _prefs.direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 42b41f2940..871266292a 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -92,29 +92,53 @@ static float directRetryCrX4ToDb(int8_t snr_x4) { static void setDirectRetryCrDefaults(NodePrefs* prefs) { prefs->direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT; prefs->direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; + prefs->direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; prefs->direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; } -static bool directRetryCrThresholdsAreValid(int8_t cr4_snr_x4, int8_t cr5_snr_x4, int8_t cr8_snr_x4) { - return (cr4_snr_x4 != 0 || cr5_snr_x4 != 0 || cr8_snr_x4 != 0) - && cr4_snr_x4 >= cr5_snr_x4 - && cr5_snr_x4 >= cr8_snr_x4; +static void setDirectRetryCrOff(NodePrefs* prefs) { + prefs->direct_retry_cr4_snr_x4 = 0; + prefs->direct_retry_cr5_snr_x4 = 0; + prefs->direct_retry_cr7_snr_x4 = 0; + prefs->direct_retry_cr8_snr_x4 = 0; +} + +static bool directRetryCrThresholdsAreOff(int8_t cr4_snr_x4, int8_t cr5_snr_x4, int8_t cr7_snr_x4, int8_t cr8_snr_x4) { + return cr4_snr_x4 == 0 && cr5_snr_x4 == 0 && cr7_snr_x4 == 0 && cr8_snr_x4 == 0; +} + +static bool directRetryCrThresholdsAreValid(int8_t cr4_snr_x4, int8_t cr5_snr_x4, int8_t cr7_snr_x4, int8_t cr8_snr_x4) { + return directRetryCrThresholdsAreOff(cr4_snr_x4, cr5_snr_x4, cr7_snr_x4, cr8_snr_x4) + || ((cr4_snr_x4 != 0 || cr5_snr_x4 != 0 || cr7_snr_x4 != 0 || cr8_snr_x4 != 0) + && cr4_snr_x4 >= cr5_snr_x4 + && cr5_snr_x4 >= cr7_snr_x4 + && cr7_snr_x4 >= cr8_snr_x4); } static void sanitizeDirectRetryCrThresholds(NodePrefs* prefs) { if (!directRetryCrThresholdsAreValid(prefs->direct_retry_cr4_snr_x4, prefs->direct_retry_cr5_snr_x4, + prefs->direct_retry_cr7_snr_x4, prefs->direct_retry_cr8_snr_x4)) { setDirectRetryCrDefaults(prefs); } } static void formatDirectRetryCrThresholds(const NodePrefs* prefs, char* reply) { - char cr4[12], cr5[12], cr8[12]; + if (directRetryCrThresholdsAreOff(prefs->direct_retry_cr4_snr_x4, + prefs->direct_retry_cr5_snr_x4, + prefs->direct_retry_cr7_snr_x4, + prefs->direct_retry_cr8_snr_x4)) { + strcpy(reply, "> off"); + return; + } + + char cr4[12], cr5[12], cr7[12], cr8[12]; strcpy(cr4, StrHelper::ftoa(directRetryCrX4ToDb(prefs->direct_retry_cr4_snr_x4))); strcpy(cr5, StrHelper::ftoa(directRetryCrX4ToDb(prefs->direct_retry_cr5_snr_x4))); + strcpy(cr7, StrHelper::ftoa(directRetryCrX4ToDb(prefs->direct_retry_cr7_snr_x4))); strcpy(cr8, StrHelper::ftoa(directRetryCrX4ToDb(prefs->direct_retry_cr8_snr_x4))); - sprintf(reply, "> %s,%s,%s,%s", cr4, cr5, cr8, cr8); + sprintf(reply, "> %s,%s,%s,%s", cr4, cr5, cr7, cr8); } static uint16_t directRetryPresetStepDefault(uint8_t preset) { @@ -198,35 +222,36 @@ static bool parseDirectRetryCrThresholds(char* value, NodePrefs* prefs) { if (value == NULL || prefs == NULL) { return false; } + if (strcmp(value, "off") == 0) { + setDirectRetryCrOff(prefs); + return true; + } - const char* parts[4]; - int num = mesh::Utils::parseTextParts(value, parts, 4); - if (num != 3 && num != 4) { + const char* parts[5]; + int num = mesh::Utils::parseTextParts(value, parts, 5); + if (num != 4) { return false; } int8_t cr4_snr_x4; int8_t cr5_snr_x4; + int8_t cr7_snr_x4; int8_t cr8_snr_x4; if (!parseDirectRetryCrDb(parts[0], cr4_snr_x4) || !parseDirectRetryCrDb(parts[1], cr5_snr_x4) - || !parseDirectRetryCrDb(parts[num == 4 ? 3 : 2], cr8_snr_x4)) { + || !parseDirectRetryCrDb(parts[2], cr7_snr_x4) + || !parseDirectRetryCrDb(parts[3], cr8_snr_x4)) { return false; } - if (num == 4) { - int8_t repeated_low_snr_x4; - if (!parseDirectRetryCrDb(parts[2], repeated_low_snr_x4) || repeated_low_snr_x4 != cr8_snr_x4) { - return false; - } - } - - if (!directRetryCrThresholdsAreValid(cr4_snr_x4, cr5_snr_x4, cr8_snr_x4)) { + if (directRetryCrThresholdsAreOff(cr4_snr_x4, cr5_snr_x4, cr7_snr_x4, cr8_snr_x4) + || !directRetryCrThresholdsAreValid(cr4_snr_x4, cr5_snr_x4, cr7_snr_x4, cr8_snr_x4)) { return false; } prefs->direct_retry_cr4_snr_x4 = cr4_snr_x4; prefs->direct_retry_cr5_snr_x4 = cr5_snr_x4; + prefs->direct_retry_cr7_snr_x4 = cr7_snr_x4; prefs->direct_retry_cr8_snr_x4 = cr8_snr_x4; return true; } @@ -323,8 +348,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { sizeof(_prefs->direct_retry_cr4_snr_x4)); // 300 retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 301 + retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, + sizeof(_prefs->direct_retry_cr7_snr_x4)); // 302 retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, - sizeof(_prefs->direct_retry_cr8_snr_x4)); // 302 + sizeof(_prefs->direct_retry_cr8_snr_x4)); // 303 // PowerSaving-only prefs stored radio_fem_rxgain at 291, before direct retry timing existed. if (radio_fem_rxgain_read != sizeof(_prefs->radio_fem_rxgain) && legacy_retry_attempts_read == sizeof(legacy_retry_attempts_or_radio_fem_rxgain) @@ -332,7 +359,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1)) { _prefs->radio_fem_rxgain = constrain(legacy_retry_attempts_or_radio_fem_rxgain, 0, 1); } - // next: 303 + // next: 304 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -386,8 +413,14 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { } else { _prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); } - if (retry_cr_read != sizeof(_prefs->direct_retry_cr4_snr_x4) + if (retry_cr_read == sizeof(_prefs->direct_retry_cr4_snr_x4) + + sizeof(_prefs->direct_retry_cr5_snr_x4) + + sizeof(_prefs->direct_retry_cr7_snr_x4)) { + _prefs->direct_retry_cr8_snr_x4 = _prefs->direct_retry_cr7_snr_x4; + sanitizeDirectRetryCrThresholds(_prefs); + } else if (retry_cr_read != sizeof(_prefs->direct_retry_cr4_snr_x4) + sizeof(_prefs->direct_retry_cr5_snr_x4) + + sizeof(_prefs->direct_retry_cr7_snr_x4) + sizeof(_prefs->direct_retry_cr8_snr_x4)) { setDirectRetryCrDefaults(_prefs); } else { @@ -467,8 +500,9 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 298 file.write((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 300 file.write((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 301 - file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 302 - // next: 303 + file.write((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 302 + file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 303 + // next: 304 file.close(); } @@ -954,7 +988,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re savePrefs(); formatDirectRetryCrThresholds(_prefs, reply); } else { - strcpy(reply, "Error, expected cr4,cr5,cr8 or cr4,cr5,low,low"); + strcpy(reply, "Error, expected off or cr4,cr5,cr7,cr8"); } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; @@ -1516,7 +1550,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep savePrefs(); formatDirectRetryCrThresholds(_prefs, reply); } else { - strcpy(reply, "Error, expected cr4,cr5,cr8 or cr4,cr5,low,low"); + strcpy(reply, "Error, expected off or cr4,cr5,cr7,cr8"); } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 43e8e6d7fb..92401b38ba 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -40,6 +40,7 @@ #define DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT 40 // 10.0 dB and up => CR4 #define DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT 30 // 7.5 dB and up => CR5 +#define DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT 10 // 2.5 dB and up => CR7 #define DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT 10 // 2.5 dB and down => CR8 #define DIRECT_RETRY_CR_SNR_X4_MIN -128 #define DIRECT_RETRY_CR_SNR_X4_MAX 127 @@ -96,6 +97,7 @@ struct NodePrefs { // persisted to file uint16_t direct_retry_step_ms; int8_t direct_retry_cr4_snr_x4; int8_t direct_retry_cr5_snr_x4; + int8_t direct_retry_cr7_snr_x4; int8_t direct_retry_cr8_snr_x4; }; From 8abf4a92ff8aa9e17b9284744807789ffe26833c Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sun, 10 May 2026 22:09:56 +0700 Subject: [PATCH 40/94] Added RADIO_FEM_RXGAIN in build option for T096 --- variants/heltec_t096/T096Board.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 54425145c4..c7f6972209 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -126,6 +126,10 @@ const char* T096Board::getManufacturerName() const { } bool T096Board::setLoRaFemLnaEnabled(bool enable) { +#if defined(RADIO_FEM_RXGAIN) && (RADIO_FEM_RXGAIN == 0) + enable = false; +#endif + if (!loRaFEMControl.isLnaCanControl()) { return false; } From 69a76b0cdae6f44fdaa4f72d39ecb3bffd15ca60 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 15 May 2026 21:07:10 +0700 Subject: [PATCH 41/94] Fixed boot loop due to flash mode for Xiao C6 --- variants/xiao_c6/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/xiao_c6/platformio.ini b/variants/xiao_c6/platformio.ini index 717be7b9a0..2aab6b7e2f 100644 --- a/variants/xiao_c6/platformio.ini +++ b/variants/xiao_c6/platformio.ini @@ -1,6 +1,7 @@ [Xiao_C6] extends = esp32c6_base board = esp32-c6-devkitm-1 +board_build.flash_mode = dio board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_flags = ${esp32c6_base.build_flags} From 19703ddbdab36239741b3ba60ad216a964ae1b86 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Thu, 21 May 2026 10:24:00 +0700 Subject: [PATCH 42/94] Added minor changes from PR1687 --- examples/simple_repeater/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 17a3c1923d..c9b9aa9163 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -152,10 +152,10 @@ void loop() { if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { #if defined(NRF52_PLATFORM) - board.sleep(1800); // nrf ignores seconds param, sleeps whenever possible + board.sleep(0); // nrf ignores seconds param, sleeps whenever possible #else if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep - board.sleep(30); // Sleep. Wake up after some seconds or when receiving a LoRa packet + board.sleep(30); // Sleep. Wake up after a while or when receiving a LoRa packet } #endif } From e59a3f5c21e268a3ec309c3bf1d133059e7502da Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Thu, 21 May 2026 10:37:09 +0700 Subject: [PATCH 43/94] Minor sleep settings for room servers --- examples/simple_room_server/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 5517980936..637a694ace 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -119,10 +119,10 @@ void loop() { if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { #if defined(NRF52_PLATFORM) - board.sleep(1800); // nrf ignores seconds param, sleeps whenever possible + board.sleep(0); // nrf ignores seconds param, sleeps whenever possible #else if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep - board.sleep(1800); // Sleep. Wake up after 30 minutes or when receiving a LoRa packet + board.sleep(30); // Sleep. Wake up after a while or when receiving a LoRa packet } #endif } From c15fdfeaf9a297446cdf53f6b6eabb05f2bc31d7 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Thu, 21 May 2026 18:54:56 +0700 Subject: [PATCH 44/94] Fixed hasPendingWork for BLE companions. --- examples/companion_radio/MyMesh.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 8a6f63c533..1b29daa012 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2223,8 +2223,5 @@ bool MyMesh::advert() { // To check if there is pending work bool MyMesh::hasPendingWork() const { -#if defined(WITH_BRIDGE) - if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep -#endif return _mgr->getOutboundTotal() > 0; } From 49b6b632e8b9fa0c5916454f16663ac6199364ce Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 22 May 2026 23:11:05 +0700 Subject: [PATCH 45/94] Supported PowerSaving for USB companion --- src/helpers/ArduinoSerialInterface.cpp | 4 ++++ src/helpers/ArduinoSerialInterface.h | 1 + 2 files changed, 5 insertions(+) diff --git a/src/helpers/ArduinoSerialInterface.cpp b/src/helpers/ArduinoSerialInterface.cpp index a01fa5866f..6b44397406 100644 --- a/src/helpers/ArduinoSerialInterface.cpp +++ b/src/helpers/ArduinoSerialInterface.cpp @@ -17,6 +17,10 @@ bool ArduinoSerialInterface::isConnected() const { return true; // no way of knowing, so assume yes } +bool ArduinoSerialInterface::isReadBusy() const { + return false; +} + bool ArduinoSerialInterface::isWriteBusy() const { return false; } diff --git a/src/helpers/ArduinoSerialInterface.h b/src/helpers/ArduinoSerialInterface.h index c4086353aa..3eed767134 100644 --- a/src/helpers/ArduinoSerialInterface.h +++ b/src/helpers/ArduinoSerialInterface.h @@ -28,6 +28,7 @@ class ArduinoSerialInterface : public BaseSerialInterface { bool isConnected() const override; + bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; From 475b3ac20bdb381752524d05bfe63bb48323177a Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 22 May 2026 23:12:22 +0700 Subject: [PATCH 46/94] Supported PowerSaving for 3 Tbeam boards --- build-iotthinks.sh | 13 ++++++--- variants/lilygo_tbeam_SX1262/platformio.ini | 27 +++++++++++++++++++ variants/lilygo_tbeam_SX1276/platformio.ini | 25 +++++++++++++++++ .../platformio.ini | 23 ++++++++++++++++ 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/build-iotthinks.sh b/build-iotthinks.sh index 9bff2eff42..502331d558 100644 --- a/build-iotthinks.sh +++ b/build-iotthinks.sh @@ -1,5 +1,5 @@ # sh ./build-repeaters-iotthinks.sh -export FIRMWARE_VERSION="PowerSaving15.0.2" +export FIRMWARE_VERSION="PowerSaving15.0.3" ############# Repeaters ############# # Commonly-used boards @@ -88,7 +88,7 @@ Heltec_t096_companion_radio_ble \ Heltec_t096_companion_radio_ble_femoff ############# Companions BLE PS ############# -# ESP32 - 13 boards +# ESP32 - 16 boards sh build.sh build-firmware \ Heltec_v3_companion_radio_ble_ps \ heltec_v4_companion_radio_ble_ps \ @@ -103,4 +103,11 @@ Heltec_Wireless_Tracker_companion_radio_ble_ps \ heltec_tracker_v2_companion_radio_ble_ps \ Heltec_Wireless_Paper_companion_radio_ble_ps \ LilyGo_TLora_V2_1_1_6_companion_radio_ble_ps \ -Heltec_ct62_companion_radio_ble_ps +Heltec_ct62_companion_radio_ble_ps \ +T_Beam_S3_Supreme_SX1262_companion_radio_ble_ps \ +Tbeam_SX1262_companion_radio_ble_ps \ +Tbeam_SX1276_companion_radio_ble_ps + +############# Companions USB ############# +sh build.sh build-firmware \ +Heltec_t096_companion_radio_usb diff --git a/variants/lilygo_tbeam_SX1262/platformio.ini b/variants/lilygo_tbeam_SX1262/platformio.ini index 1585dd74d3..cfd8557f35 100644 --- a/variants/lilygo_tbeam_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_SX1262/platformio.ini @@ -65,6 +65,33 @@ lib_deps = ${LilyGo_TBeam_SX1262.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Tbeam_SX1262_companion_radio_ble_ps] +extends = LilyGo_TBeam_SX1262 +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +board_build.upload.maximum_ram_size=2000000 +build_flags = + ${LilyGo_TBeam_SX1262.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +; -D RADIOLIB_DEBUG_BASIC=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TBeam_SX1262.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_TBeam_SX1262.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Tbeam_SX1262_repeater] extends = LilyGo_TBeam_SX1262 build_flags = diff --git a/variants/lilygo_tbeam_SX1276/platformio.ini b/variants/lilygo_tbeam_SX1276/platformio.ini index 7482ef7bd8..a134335c8d 100644 --- a/variants/lilygo_tbeam_SX1276/platformio.ini +++ b/variants/lilygo_tbeam_SX1276/platformio.ini @@ -61,6 +61,31 @@ lib_deps = ${LilyGo_TBeam_SX1276.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Tbeam_SX1276_companion_radio_ble_ps] +extends = LilyGo_TBeam_SX1276 +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +board_build.upload.maximum_ram_size=2000000 +build_flags = + ${LilyGo_TBeam_SX1276.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=160 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 +; -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +; -D RADIOLIB_DEBUG_BASIC=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TBeam_SX1276.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_TBeam_SX1276.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Tbeam_SX1276_repeater] extends = LilyGo_TBeam_SX1276 build_flags = diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index 249e68713b..48a9ddc66c 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -142,6 +142,29 @@ lib_deps = ${T_Beam_S3_Supreme_SX1262.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:T_Beam_S3_Supreme_SX1262_companion_radio_ble_ps] +extends = T_Beam_S3_Supreme_SX1262 +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${T_Beam_S3_Supreme_SX1262.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=8 +; -D MESH_DEBUG=1 +build_src_filter = ${T_Beam_S3_Supreme_SX1262.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${T_Beam_S3_Supreme_SX1262.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:T_Beam_S3_Supreme_SX1262_companion_radio_wifi] extends = T_Beam_S3_Supreme_SX1262 build_flags = From b59966b6faef86daa58e7d2d03de3d9e18308a9c Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 22 May 2026 14:16:14 -0700 Subject: [PATCH 47/94] Add direct path override support --- docs/cli_commands.md | 27 +++ examples/simple_repeater/MyMesh.cpp | 320 ++++++++++++++++++++++++++-- examples/simple_repeater/MyMesh.h | 19 +- src/Mesh.cpp | 2 +- src/Mesh.h | 5 + src/helpers/ClientACL.cpp | 13 +- src/helpers/ClientACL.h | 5 +- 7 files changed, 373 insertions(+), 18 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 920c0fd7ff..c838fd1065 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -134,6 +134,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - If a full failure has no row yet, it first seeds the row at the active retry cutoff + `2.5 dB`, then applies the `0.25 dB` penalty. - Serial CLI page size is fixed at `128` rows; choose page with `get recent.repeater `. - Over LoRa remote CLI, page size is fixed at `7` rows; choose page with `get recent.repeater `. +- Repeaters can use adjacent entries in this table to short-circuit non-TRACE direct packets when this node appears later in the direct path. --- @@ -801,6 +802,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Default:** `2` for `rooftop`, `1` for `infra` and `mobile` +**Note:** Prefixes in `flood.retry.ignore` do not count toward this path length. + --- #### View or change whether advert packets are flood-retried @@ -900,6 +903,30 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or set direct path overrides for the current remote client +**Usage:** +- `get outpath` +- `set outpath ` +- `set outpath clear` +- `set outpath flood` +- `get altpath` +- `set altpath ` +- `set altpath clear` + +**Parameters:** +- `hopN_hex`: Hop hash, `2`, `4`, or `6` hex characters. All hops must use the same width. + +**Notes:** +- These commands require remote client context; they target the caller's ACL entry. +- The path hash size is inferred from the hop hash width. +- `outpath` overrides the primary direct route used for replies to the caller. +- `clear` forgets the current direct path and allows normal path discovery to repopulate it. +- `flood` forces replies to use flood packets until the client logs in again. +- `altpath` is an optional second direct route used for duplicate response attempts. +- `set altpath clear` removes the duplicate route so only one reply is sent. + +--- + #### View or change this room server's 'read-only' flag **Usage:** - `get allow.read.only` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index d8e72cde80..5071eb8c1f 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -529,6 +529,64 @@ void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, ui } } +static bool pathsEqual(const uint8_t* a, uint8_t a_len, const uint8_t* b, uint8_t b_len) { + if (a == NULL || b == NULL || a_len != b_len || !mesh::Packet::isValidPathLen(a_len)) return false; + + uint8_t hash_count = a_len & 63; + uint8_t hash_size = (a_len >> 6) + 1; + return memcmp(a, b, hash_count * hash_size) == 0; +} + +static bool hasUsablePath(const uint8_t* path, uint8_t path_len) { + return path != NULL && mesh::Packet::isValidPathLen(path_len) && (path_len & 63) > 0; +} + +mesh::Packet* MyMesh::createPacketCopy(const mesh::Packet* packet, const char* caller) { + if (packet == NULL) return NULL; + + mesh::Packet* copy = obtainNewPacket(); + if (copy == NULL) { + MESH_DEBUG_PRINTLN("%s %s: error, packet pool empty", getLogDateTime(), caller); + return NULL; + } + *copy = *packet; + return copy; +} + +mesh::Packet* MyMesh::createAltPathCopy(const mesh::Packet* packet, + const uint8_t* primary_path, uint8_t primary_path_len, + const uint8_t* alt_path, uint8_t alt_path_len) { + if (!hasUsablePath(alt_path, alt_path_len)) return NULL; + if (hasUsablePath(primary_path, primary_path_len) && pathsEqual(primary_path, primary_path_len, alt_path, alt_path_len)) { + return NULL; + } + return createPacketCopy(packet, "MyMesh::createAltPathCopy()"); +} + +void MyMesh::sendFloodReplyWithAltPath(mesh::Packet* packet, + const uint8_t* direct_path, uint8_t direct_path_len, + const uint8_t* alt_path, uint8_t alt_path_len, + unsigned long delay_millis, uint8_t path_hash_size) { + mesh::Packet* direct = hasUsablePath(direct_path, direct_path_len) + ? createPacketCopy(packet, "MyMesh::sendFloodReplyWithAltPath(direct)") + : NULL; + mesh::Packet* alt = createAltPathCopy(packet, direct_path, direct_path_len, alt_path, alt_path_len); + + sendFloodReply(packet, delay_millis, path_hash_size); + if (direct) sendDirect(direct, direct_path, direct_path_len, delay_millis); + if (alt) sendDirect(alt, alt_path, alt_path_len, delay_millis); +} + +void MyMesh::sendDirectWithAltPath(mesh::Packet* packet, + const uint8_t* path, uint8_t path_len, + const uint8_t* alt_path, uint8_t alt_path_len, + uint32_t delay_millis) { + mesh::Packet* alt = createAltPathCopy(packet, path, path_len, alt_path, alt_path_len); + + sendDirect(packet, path, path_len, delay_millis); + if (alt) sendDirect(alt, alt_path, alt_path_len, delay_millis); +} + bool MyMesh::allowPacketForward(const mesh::Packet *packet) { if (_prefs.disable_fwd) return false; @@ -849,6 +907,54 @@ bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_ho return true; } +bool MyMesh::maybeShortCircuitDirect(mesh::Packet* packet) { + if (packet == NULL || !packet->isRouteDirect() || packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { + return false; + } + + uint8_t hash_count = packet->getPathHashCount(); + uint8_t hash_size = packet->getPathHashSize(); + if (hash_count < 2 || hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { + return false; + } + + // Normal direct forwarding handles the first path entry. Short-circuit only when we are later in the route. + int self_idx = -1; + for (uint8_t i = 1; i < hash_count; i++) { + if (self_id.isHashMatch(&packet->path[i * hash_size], hash_size)) { + self_idx = i; + break; + } + } + if (self_idx < 0) { + return false; + } + + const SimpleMeshTables* tables = (const SimpleMeshTables*)getTables(); + bool adjacent_recent = false; + if (self_idx > 0 + && tables->findRecentRepeaterByHash(&packet->path[(self_idx - 1) * hash_size], hash_size) != NULL) { + adjacent_recent = true; + } + if (!adjacent_recent && self_idx + 1 < hash_count + && tables->findRecentRepeaterByHash(&packet->path[(self_idx + 1) * hash_size], hash_size) != NULL) { + adjacent_recent = true; + } + if (!adjacent_recent) { + return false; + } + + uint8_t remaining = hash_count - (uint8_t)self_idx; + memmove(packet->path, &packet->path[self_idx * hash_size], remaining * hash_size); + packet->setPathHashCount(remaining); + + MESH_DEBUG_PRINTLN("%s direct short-circuit (type=%d, original_hop=%d, remaining_hops=%d)", + getLogDateTime(), + (uint32_t)packet->getPayloadType(), + self_idx + 1, + (uint32_t)remaining); + return true; +} uint8_t MyMesh::getDirectRetryCodingRateForSNR(int8_t snr_x4) const { if (_prefs.direct_retry_cr4_snr_x4 == 0 && _prefs.direct_retry_cr5_snr_x4 == 0 @@ -992,6 +1098,31 @@ bool MyMesh::floodRetryPrefixIgnored(const uint8_t* prefix, uint8_t prefix_len) } return false; } +uint8_t MyMesh::floodRetryEffectivePathLength(const mesh::Packet* packet, uint8_t max_hops) const { + if (packet == NULL || !packet->isRouteFlood() || packet->getPathHashCount() == 0) { + return 0; + } + + uint8_t hash_size = packet->getPathHashSize(); + if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { + return packet->getPathHashCount(); + } + + uint8_t hop_count = packet->getPathHashCount(); + if (max_hops < hop_count) { + hop_count = max_hops; + } + + uint8_t effective_len = 0; + const uint8_t* path = packet->path; + for (uint8_t hop = 0; hop < hop_count; hop++) { + if (!floodRetryPrefixIgnored(path, hash_size)) { + effective_len++; + } + path += hash_size; + } + return effective_len; +} bool MyMesh::floodRetryPrefixFresh(const uint8_t* prefix, uint8_t prefix_len) const { const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(prefix, prefix_len); if (recent == NULL || recent->last_heard_millis == 0) { @@ -1413,7 +1544,15 @@ uint8_t MyMesh::getFloodRetryMaxPathLength(const mesh::Packet* packet) const { if (gate == FLOOD_RETRY_PATH_GATE_DISABLED) { return FLOOD_RETRY_PATH_GATE_DISABLED; } - return gate <= 63 ? gate : 2; + if (gate > 63) { + gate = 2; + } + + uint8_t raw_hops = packet != NULL ? packet->getPathHashCount() : 0; + uint8_t effective_hops = floodRetryEffectivePathLength(packet); + uint8_t ignored_hops = raw_hops > effective_hops ? raw_hops - effective_hops : 0; + uint16_t adjusted_gate = (uint16_t)gate + ignored_hops; + return adjusted_gate > 63 ? 63 : (uint8_t)adjusted_gate; } uint8_t MyMesh::getFloodRetryMaxAttempts(const mesh::Packet* packet) const { if (_prefs.disable_fwd) { @@ -1447,7 +1586,7 @@ bool MyMesh::isFloodRetryEchoTarget(const mesh::Packet* packet, uint8_t progress if (hasFloodRetryPrefixes()) { return floodRetryLastHopMatches(packet); } - if (packet->getPathHashCount() <= progress_marker) { + if (floodRetryEffectivePathLength(packet) <= floodRetryEffectivePathLength(packet, progress_marker)) { return false; } return true; @@ -1607,13 +1746,22 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + if (path) { + if (hasUsablePath(client->out_path, client->out_path_len)) { + sendFloodReplyWithAltPath(path, client->out_path, client->out_path_len, + client->alt_path, client->alt_path_len, + SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + } else { + sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + } + } } else { mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); if (reply) { - if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT - sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); + if (hasUsablePath(client->out_path, client->out_path_len)) { // we have an out_path, so send DIRECT + sendDirectWithAltPath(reply, client->out_path, client->out_path_len, + client->alt_path, client->alt_path_len, SERVER_RESPONSE_DELAY); } else { sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } @@ -1645,10 +1793,11 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, mesh::Packet *ack = createAck(ack_hash); if (ack) { - if (client->out_path_len == OUT_PATH_UNKNOWN) { - sendFloodReply(ack, TXT_ACK_DELAY, packet->getPathHashSize()); + if (hasUsablePath(client->out_path, client->out_path_len)) { + sendDirectWithAltPath(ack, client->out_path, client->out_path_len, + client->alt_path, client->alt_path_len, TXT_ACK_DELAY); } else { - sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY); + sendFloodReply(ack, TXT_ACK_DELAY, packet->getPathHashSize()); } } } @@ -1659,7 +1808,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, if (is_retry) { *reply = 0; } else { - handleCommand(sender_timestamp, command, reply); + handleCommand(sender_timestamp, client, command, reply); } int text_len = strlen(reply); if (text_len > 0) { @@ -1673,10 +1822,11 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); if (reply) { - if (client->out_path_len == OUT_PATH_UNKNOWN) { - sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); + if (hasUsablePath(client->out_path, client->out_path_len)) { + sendDirectWithAltPath(reply, client->out_path, client->out_path_len, + client->alt_path, client->alt_path_len, CLI_REPLY_DELAY_MILLIS); } else { - sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS); + sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); } } } @@ -1696,7 +1846,9 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t auto client = acl.getClientByIdx(i); // store a copy of path, for sendDirect() - client->out_path_len = mesh::Packet::copyPath(client->out_path, path, path_len); + if (client->out_path_len != OUT_PATH_FORCE_FLOOD) { + client->out_path_len = mesh::Packet::copyPath(client->out_path, path, path_len); + } client->last_activity = getRTCClock()->getCurrentTime(); } else { MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i); @@ -2127,7 +2279,100 @@ void MyMesh::clearStats() { ((SimpleMeshTables *)getTables())->resetStats(); } -void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) { +static char* trimSpaces(char* s) { + while (*s == ' ') s++; + char* end = s + strlen(s); + while (end > s && end[-1] == ' ') end--; + *end = 0; + return s; +} + +static bool parsePathCommand(char* raw, uint8_t* out_path, uint8_t& out_path_len, const char*& err) { + if (raw == NULL || out_path == NULL) { + err = "Err - bad params"; + return false; + } + + char* spec = trimSpaces(raw); + if (*spec == 0) { + err = "Err - missing path"; + return false; + } + if (strcmp(spec, "clear") == 0 || strcmp(spec, "-") == 0 || strcmp(spec, "none") == 0) { + out_path_len = OUT_PATH_UNKNOWN; + return true; + } + if (strcmp(spec, "flood") == 0) { + out_path_len = OUT_PATH_FORCE_FLOOD; + return true; + } + + uint8_t hash_size = 0; + uint8_t hop_count = 0; + char* token = spec; + while (token && *token) { + char* comma = strchr(token, ','); + if (comma) *comma = 0; + token = trimSpaces(token); + + int hex_len = strlen(token); + if (!(hex_len == 2 || hex_len == 4 || hex_len == 6)) { + err = "Err - each hop must be 1/2/3 bytes hex"; + return false; + } + + uint8_t hop_hash_size = (uint8_t)(hex_len / 2); + if (hash_size == 0) { + hash_size = hop_hash_size; + } else if (hash_size != hop_hash_size) { + err = "Err - mixed hash sizes in path"; + return false; + } + + if (hop_count >= 63 || (hop_count + 1) * hash_size > MAX_PATH_SIZE) { + err = "Err - path too long"; + return false; + } + if (!mesh::Utils::fromHex(&out_path[hop_count * hash_size], hash_size, token)) { + err = "Err - bad hex"; + return false; + } + + hop_count++; + token = comma ? comma + 1 : NULL; + } + + if (hash_size == 0 || hop_count == 0) { + err = "Err - missing path"; + return false; + } + out_path_len = ((hash_size - 1) << 6) | (hop_count & 63); + return true; +} + +static void formatPathReply(const uint8_t* path, uint8_t path_len, char* out, size_t out_len) { + if (path_len == OUT_PATH_FORCE_FLOOD) { + snprintf(out, out_len, "> flood"); + return; + } + if (path_len == OUT_PATH_UNKNOWN) { + snprintf(out, out_len, "> unknown"); + return; + } + if (!mesh::Packet::isValidPathLen(path_len)) { + snprintf(out, out_len, "> invalid"); + return; + } + + uint8_t hash_size = (path_len >> 6) + 1; + uint8_t hop_count = path_len & 63; + uint8_t byte_len = hop_count * hash_size; + char hex[(MAX_PATH_SIZE * 2) + 1]; + mesh::Utils::toHex(hex, path, byte_len); + snprintf(out, out_len, "> hs=%u hops=%u hex=%s", (uint32_t)hash_size, (uint32_t)hop_count, hex); +} + +void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *command, char *reply) { if (region_load_active) { if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation region_map = temp_map; // copy over the temp instance as new current map @@ -2204,6 +2449,53 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply Serial.printf("\n"); } reply[0] = 0; + } else if (strcmp(command, "get outpath") == 0 + || strcmp(command, "set outpath") == 0 + || strncmp(command, "set outpath ", 12) == 0 + || strcmp(command, "get altpath") == 0 + || strcmp(command, "set altpath") == 0 + || strncmp(command, "set altpath ", 12) == 0) { + bool is_get = strncmp(command, "get ", 4) == 0; + bool is_alt = strstr(command, "altpath") != NULL; + if (sender == NULL) { + strcpy(reply, "Err - command needs remote client context"); + } else if (is_get) { + formatPathReply(is_alt ? sender->alt_path : sender->out_path, + is_alt ? sender->alt_path_len : sender->out_path_len, + reply, 160); + } else { + char* spec = command + 11; // length of "set outpath"/"set altpath" + if (*spec == ' ') spec++; + + uint8_t path[MAX_PATH_SIZE]; + uint8_t path_len = OUT_PATH_UNKNOWN; + const char* err = NULL; + if (!parsePathCommand(spec, path, path_len, err)) { + strcpy(reply, err ? err : "Err - invalid path"); + } else if (is_alt && path_len == OUT_PATH_FORCE_FLOOD) { + strcpy(reply, "Err - bad params"); + } else { + if (is_alt) { + if (path_len == OUT_PATH_UNKNOWN) { + memset(sender->alt_path, 0, sizeof(sender->alt_path)); + sender->alt_path_len = OUT_PATH_UNKNOWN; + } else { + sender->alt_path_len = mesh::Packet::copyPath(sender->alt_path, path, path_len); + } + } else { + if (path_len == OUT_PATH_UNKNOWN || path_len == OUT_PATH_FORCE_FLOOD) { + memset(sender->out_path, 0, sizeof(sender->out_path)); + sender->out_path_len = path_len; + } else { + sender->out_path_len = mesh::Packet::copyPath(sender->out_path, path, path_len); + } + } + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + formatPathReply(is_alt ? sender->alt_path : sender->out_path, + is_alt ? sender->alt_path_len : sender->out_path_len, + reply, 160); + } + } } else if (strncmp(command, "get recent.repeater", 19) == 0 || strncmp(command, "set recent.repeater", 19) == 0 || strncmp(command, "clear recent.repeater", 21) == 0 diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 73f8055662..0e6c5f7009 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -141,6 +141,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { bool floodRetryPrefixMatches(const mesh::Packet* packet) const; bool floodRetryLastHopMatches(const mesh::Packet* packet) const; bool floodRetryPrefixIgnored(const uint8_t* prefix, uint8_t prefix_len) const; + uint8_t floodRetryEffectivePathLength(const mesh::Packet* packet, uint8_t max_hops = 0xFF) const; bool floodRetryPrefixFresh(const uint8_t* prefix, uint8_t prefix_len) const; int floodRetryBucketForPrefix(const uint8_t* prefix, uint8_t prefix_len, bool require_fresh, bool include_other) const; @@ -184,6 +185,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; uint8_t getDefaultTxCodingRate() const override { return active_cr; } bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override; + bool maybeShortCircuitDirect(mesh::Packet* packet) override; void configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) override; uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override; uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override; @@ -223,6 +225,18 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void onControlDataRecv(mesh::Packet* packet) override; void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size); + mesh::Packet* createPacketCopy(const mesh::Packet* packet, const char* caller); + mesh::Packet* createAltPathCopy(const mesh::Packet* packet, + const uint8_t* primary_path, uint8_t primary_path_len, + const uint8_t* alt_path, uint8_t alt_path_len); + void sendFloodReplyWithAltPath(mesh::Packet* packet, + const uint8_t* direct_path, uint8_t direct_path_len, + const uint8_t* alt_path, uint8_t alt_path_len, + unsigned long delay_millis, uint8_t path_hash_size); + void sendDirectWithAltPath(mesh::Packet* packet, + const uint8_t* path, uint8_t path_len, + const uint8_t* alt_path, uint8_t alt_path_len, + uint32_t delay_millis); public: MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); @@ -272,7 +286,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void saveIdentity(const mesh::LocalIdentity& new_id) override; void clearStats() override; - void handleCommand(uint32_t sender_timestamp, char* command, char* reply); + void handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char* command, char* reply); + void handleCommand(uint32_t sender_timestamp, char* command, char* reply) { + handleCommand(sender_timestamp, NULL, command, reply); + } void loop(); #if defined(WITH_BRIDGE) diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 941c71a106..8b088b3171 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -254,7 +254,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } } - if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) && allowPacketForward(pkt)) { + if ((self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) || maybeShortCircuitDirect(pkt)) && allowPacketForward(pkt)) { if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { return forwardMultipartDirect(pkt); } else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { diff --git a/src/Mesh.h b/src/Mesh.h index 3f86653e47..483d48260e 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -135,6 +135,11 @@ class Mesh : public Dispatcher { */ virtual bool allowDirectRetry(const Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const; + /** + * \brief Allow subclasses to rewrite a non-TRACE DIRECT packet path when this node can safely skip ahead. + */ + virtual bool maybeShortCircuitDirect(Packet* packet) { return false; } + /** * \returns milliseconds to wait for the next-hop echo before queueing one retry of the DIRECT packet. */ diff --git a/src/helpers/ClientACL.cpp b/src/helpers/ClientACL.cpp index 1282382737..1d88082349 100644 --- a/src/helpers/ClientACL.cpp +++ b/src/helpers/ClientACL.cpp @@ -1,5 +1,7 @@ #include "ClientACL.h" +static const uint8_t CONTACT_RECORD_VERSION_ALT_PATH = 1; + static File openWrite(FILESYSTEM* _fs, const char* filename) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) _fs->remove(filename); @@ -28,6 +30,7 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) { uint8_t unused[2]; memset(&c, 0, sizeof(c)); + c.alt_path_len = OUT_PATH_UNKNOWN; bool success = (file.read(pub_key, 32) == 32); success = success && (file.read((uint8_t *) &c.permissions, 1) == 1); @@ -36,6 +39,10 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) { success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1); success = success && (file.read(c.out_path, 64) == 64); success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); // will be recalculated below + if (success && unused[0] >= CONTACT_RECORD_VERSION_ALT_PATH) { + success = success && (file.read((uint8_t *)&c.alt_path_len, 1) == 1); + success = success && (file.read(c.alt_path, 64) == 64); + } if (!success) break; // EOF @@ -57,7 +64,8 @@ void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) { File file = openWrite(_fs, "/s_contacts"); if (file) { uint8_t unused[2]; - memset(unused, 0, sizeof(unused)); + unused[0] = CONTACT_RECORD_VERSION_ALT_PATH; + unused[1] = 0; for (int i = 0; i < num_clients; i++) { auto c = &clients[i]; @@ -70,6 +78,8 @@ void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) { success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1); success = success && (file.write(c->out_path, 64) == 64); success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); + success = success && (file.write((uint8_t *)&c->alt_path_len, 1) == 1); + success = success && (file.write(c->alt_path, 64) == 64); if (!success) break; // write failed } @@ -115,6 +125,7 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) { c->permissions = init_perms; c->id = id; c->out_path_len = OUT_PATH_UNKNOWN; + c->alt_path_len = OUT_PATH_UNKNOWN; return c; } diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index b758f7068d..356574de53 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -10,13 +10,16 @@ #define PERM_ACL_READ_WRITE 2 #define PERM_ACL_ADMIN 3 -#define OUT_PATH_UNKNOWN 0xFF +#define OUT_PATH_FORCE_FLOOD 0xFE +#define OUT_PATH_UNKNOWN 0xFF struct ClientInfo { mesh::Identity id; uint8_t permissions; uint8_t out_path_len; uint8_t out_path[MAX_PATH_SIZE]; + uint8_t alt_path_len; + uint8_t alt_path[MAX_PATH_SIZE]; uint8_t shared_secret[PUB_KEY_SIZE]; uint32_t last_timestamp; // by THEIR clock (transient) uint32_t last_activity; // by OUR clock (transient) From c0e3860c3c321662a0e508276d49c5f7b0d2f738 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 22 May 2026 14:21:10 -0700 Subject: [PATCH 48/94] Add repeater flood text command --- docs/cli_commands.md | 11 +++++ examples/simple_repeater/MyMesh.cpp | 63 +++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index c838fd1065..d200cdd105 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -115,6 +115,17 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +### Send flood text to `#repeaters` channel + +**Usage:** +- `send text.flood ` + +**Notes:** +- Sends a `PAYLOAD_TYPE_GRP_TXT` flood message using the built-in `#repeaters` channel key. +- Message format is `: `. + +--- + ### Get or set recent repeater fallback prefix/SNR **Usage:** - `get recent.repeater` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 5071eb8c1f..a2fb316228 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -63,6 +63,9 @@ #define CLI_REPLY_DELAY_MILLIS 600 #define LAZY_CONTACTS_WRITE_DELAY 5000 +#ifndef REPEATERS_CHANNEL_KEY_HEX + #define REPEATERS_CHANNEL_KEY_HEX "89db441e2814dccf0dbd2e8cc5f501a3" +#endif static void formatRecentRepeaterPrefix(const SimpleMeshTables::RecentRepeaterInfo* info, char* out, size_t out_len) { if (out == NULL || out_len == 0) { @@ -541,6 +544,22 @@ static bool hasUsablePath(const uint8_t* path, uint8_t path_len) { return path != NULL && mesh::Packet::isValidPathLen(path_len) && (path_len & 63) > 0; } +static bool buildRepeatersChannel(mesh::GroupChannel& channel) { + const char* hex = REPEATERS_CHANNEL_KEY_HEX; + size_t hex_len = strlen(hex); + if (!(hex_len == 32 || hex_len == 64)) return false; + for (size_t i = 0; i < hex_len; i++) { + if (!mesh::Utils::isHexChar(hex[i])) return false; + } + + memset(channel.secret, 0, sizeof(channel.secret)); + size_t key_len = hex_len / 2; + if (!mesh::Utils::fromHex(channel.secret, key_len, hex)) return false; + + mesh::Utils::sha256(channel.hash, sizeof(channel.hash), channel.secret, key_len); + return true; +} + mesh::Packet* MyMesh::createPacketCopy(const mesh::Packet* packet, const char* caller) { if (packet == NULL) return NULL; @@ -2496,6 +2515,50 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char * reply, 160); } } + } else if (strncmp(command, "send text.flood ", 16) == 0) { + char* text = trimSpaces(command + 16); + if (*text == 0) { + strcpy(reply, "Err - usage: send text.flood "); + } else { + mesh::GroupChannel channel; + if (!buildRepeatersChannel(channel)) { + strcpy(reply, "Err - invalid #repeaters key"); + } else { + uint8_t temp[MAX_PACKET_PAYLOAD]; + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); + memcpy(temp, ×tamp, 4); + temp[4] = (TXT_TYPE_PLAIN << 2); + + const size_t max_data_len = MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE; + const size_t prefix_cap = max_data_len > 5 ? max_data_len - 5 + 1 : 0; + int prefix_written = prefix_cap > 0 + ? snprintf((char*)&temp[5], prefix_cap, "%s: ", _prefs.node_name) + : -1; + if (prefix_written < 0) { + strcpy(reply, "Err - unable to create message"); + } else { + size_t prefix_len = (size_t)prefix_written; + if (prefix_len >= prefix_cap) { + prefix_len = prefix_cap - 1; + } + + size_t text_len = strlen(text); + size_t max_text_len = max_data_len - 5 - prefix_len; + if (text_len > max_text_len) { + text_len = max_text_len; + } + memcpy(&temp[5 + prefix_len], text, text_len); + + auto pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, channel, temp, 5 + prefix_len + text_len); + if (pkt) { + sendFloodScoped(default_scope, pkt, 0, _prefs.path_hash_mode + 1); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - unable to create packet"); + } + } + } + } } else if (strncmp(command, "get recent.repeater", 19) == 0 || strncmp(command, "set recent.repeater", 19) == 0 || strncmp(command, "clear recent.repeater", 21) == 0 From f693bdb2128383dbe707925ce12954669d3fa96b Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 22 May 2026 14:28:03 -0700 Subject: [PATCH 49/94] Add opt-in battery alert flood text --- docs/cli_commands.md | 17 +++ examples/simple_repeater/MyMesh.cpp | 163 +++++++++++++++++++++------- examples/simple_repeater/MyMesh.h | 5 + src/helpers/CommonCLI.cpp | 11 +- src/helpers/CommonCLI.h | 1 + 5 files changed, 158 insertions(+), 39 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index d200cdd105..e8d7a20a68 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -126,6 +126,23 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +### View or change automatic low-battery alerts to `#repeaters` + +**Usage:** +- `get battery.alert` +- `set battery.alert ` + +**Parameters:** +- `state`: `on` (enable) or `off` (disable) + +**Default:** `off` + +**Notes:** +- When enabled, sends a `#repeaters` flood text warning if voltage is above `1 V` and the battery estimate is below `20%`. +- Warnings repeat every `24` hours, or every `12` hours below `10%`. + +--- + ### Get or set recent repeater fallback prefix/SNR **Usage:** - `get recent.repeater` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index a2fb316228..6dc8bb7d74 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -66,6 +66,19 @@ #ifndef REPEATERS_CHANNEL_KEY_HEX #define REPEATERS_CHANNEL_KEY_HEX "89db441e2814dccf0dbd2e8cc5f501a3" #endif +#ifndef BATT_MIN_MILLIVOLTS + #define BATT_MIN_MILLIVOLTS 3000 +#endif +#ifndef BATT_MAX_MILLIVOLTS + #define BATT_MAX_MILLIVOLTS 4200 +#endif + +#define LOW_BATTERY_MIN_VALID_MV 1000 +#define LOW_BATTERY_WARN_PERCENT 20 +#define LOW_BATTERY_CRITICAL_PERCENT 10 +#define LOW_BATTERY_CHECK_INTERVAL (60UL * 1000UL) +#define LOW_BATTERY_WARN_INTERVAL (24UL * 60UL * 60UL * 1000UL) +#define LOW_BATTERY_CRITICAL_INTERVAL (12UL * 60UL * 60UL * 1000UL) static void formatRecentRepeaterPrefix(const SimpleMeshTables::RecentRepeaterInfo* info, char* out, size_t out_len) { if (out == NULL || out_len == 0) { @@ -560,6 +573,17 @@ static bool buildRepeatersChannel(mesh::GroupChannel& channel) { return true; } +static uint8_t batteryPercentFromMilliVolts(uint16_t batt_mv) { + const int min_mv = BATT_MIN_MILLIVOLTS; + const int max_mv = BATT_MAX_MILLIVOLTS; + if (max_mv <= min_mv) return 100; + + int pct = (((int)batt_mv - min_mv) * 100) / (max_mv - min_mv); + if (pct < 0) return 0; + if (pct > 100) return 100; + return (uint8_t)pct; +} + mesh::Packet* MyMesh::createPacketCopy(const mesh::Packet* packet, const char* caller) { if (packet == NULL) return NULL; @@ -1970,6 +1994,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc last_millis = 0; uptime_millis = 0; next_local_advert = next_flood_advert = 0; + next_battery_alert_check = 0; + last_battery_alert_sent = 0; + battery_alert_sent = false; dirty_contacts_expiry = 0; set_radio_at = revert_radio_at = 0; _logging = false; @@ -1996,6 +2023,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_retry_path_gate = 2; _prefs.flood_retry_bridge_enabled = 0; _prefs.flood_retry_advert_enabled = 0; + _prefs.battery_alert_enabled = 0; _prefs.direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT; _prefs.direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; _prefs.direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; @@ -2114,6 +2142,82 @@ void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint3 } } +bool MyMesh::sendRepeatersFloodText(const char* text) { + if (text == NULL || *text == 0) return false; + + mesh::GroupChannel channel; + if (!buildRepeatersChannel(channel)) { + return false; + } + + uint8_t temp[MAX_PACKET_PAYLOAD]; + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); + memcpy(temp, ×tamp, 4); + temp[4] = (TXT_TYPE_PLAIN << 2); + + const size_t max_data_len = MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE; + const size_t prefix_cap = max_data_len > 5 ? max_data_len - 5 + 1 : 0; + int prefix_written = prefix_cap > 0 + ? snprintf((char*)&temp[5], prefix_cap, "%s: ", _prefs.node_name) + : -1; + if (prefix_written < 0) { + return false; + } + + size_t prefix_len = (size_t)prefix_written; + if (prefix_len >= prefix_cap) { + prefix_len = prefix_cap - 1; + } + + size_t text_len = strlen(text); + size_t max_text_len = max_data_len - 5 - prefix_len; + if (text_len > max_text_len) { + text_len = max_text_len; + } + memcpy(&temp[5 + prefix_len], text, text_len); + + auto pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, channel, temp, 5 + prefix_len + text_len); + if (pkt == NULL) { + return false; + } + + sendFloodScoped(default_scope, pkt, 0, _prefs.path_hash_mode + 1); + return true; +} + +void MyMesh::checkBatteryAlert() { + if (!_prefs.battery_alert_enabled) { + battery_alert_sent = false; + return; + } + + if (next_battery_alert_check && !millisHasNowPassed(next_battery_alert_check)) { + return; + } + next_battery_alert_check = futureMillis(LOW_BATTERY_CHECK_INTERVAL); + + uint16_t batt_mv = board.getBattMilliVolts(); + uint8_t batt_pct = batteryPercentFromMilliVolts(batt_mv); + if (batt_mv <= LOW_BATTERY_MIN_VALID_MV || batt_pct >= LOW_BATTERY_WARN_PERCENT) { + battery_alert_sent = false; + return; + } + + unsigned long interval = batt_pct < LOW_BATTERY_CRITICAL_PERCENT + ? LOW_BATTERY_CRITICAL_INTERVAL + : LOW_BATTERY_WARN_INTERVAL; + if (battery_alert_sent && !millisHasNowPassed(last_battery_alert_sent + interval)) { + return; + } + + char text[96]; + snprintf(text, sizeof(text), "LOW BATTERY %u%% (%u mV)", (uint32_t)batt_pct, (uint32_t)batt_mv); + if (sendRepeatersFloodText(text)) { + battery_alert_sent = true; + last_battery_alert_sent = millis(); + } +} + void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) { set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params pending_freq = freq; @@ -2519,45 +2623,27 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char * char* text = trimSpaces(command + 16); if (*text == 0) { strcpy(reply, "Err - usage: send text.flood "); + } else if (sendRepeatersFloodText(text)) { + strcpy(reply, "OK"); } else { - mesh::GroupChannel channel; - if (!buildRepeatersChannel(channel)) { - strcpy(reply, "Err - invalid #repeaters key"); - } else { - uint8_t temp[MAX_PACKET_PAYLOAD]; - uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); - memcpy(temp, ×tamp, 4); - temp[4] = (TXT_TYPE_PLAIN << 2); - - const size_t max_data_len = MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE; - const size_t prefix_cap = max_data_len > 5 ? max_data_len - 5 + 1 : 0; - int prefix_written = prefix_cap > 0 - ? snprintf((char*)&temp[5], prefix_cap, "%s: ", _prefs.node_name) - : -1; - if (prefix_written < 0) { - strcpy(reply, "Err - unable to create message"); - } else { - size_t prefix_len = (size_t)prefix_written; - if (prefix_len >= prefix_cap) { - prefix_len = prefix_cap - 1; - } - - size_t text_len = strlen(text); - size_t max_text_len = max_data_len - 5 - prefix_len; - if (text_len > max_text_len) { - text_len = max_text_len; - } - memcpy(&temp[5 + prefix_len], text, text_len); - - auto pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, channel, temp, 5 + prefix_len + text_len); - if (pkt) { - sendFloodScoped(default_scope, pkt, 0, _prefs.path_hash_mode + 1); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Err - unable to create packet"); - } - } - } + strcpy(reply, "Err - unable to create packet"); + } + } else if (strcmp(command, "get battery.alert") == 0) { + sprintf(reply, "> %s", _prefs.battery_alert_enabled ? "on" : "off"); + } else if (strncmp(command, "set battery.alert ", 18) == 0) { + const char* value = command + 18; + if (strcmp(value, "on") == 0) { + _prefs.battery_alert_enabled = 1; + next_battery_alert_check = 0; + savePrefs(); + strcpy(reply, "OK"); + } else if (strcmp(value, "off") == 0) { + _prefs.battery_alert_enabled = 0; + battery_alert_sent = false; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - usage: set battery.alert "); } } else if (strncmp(command, "get recent.repeater", 19) == 0 || strncmp(command, "set recent.repeater", 19) == 0 @@ -2759,6 +2845,7 @@ void MyMesh::loop() { #endif mesh::Mesh::loop(); + checkBatteryAlert(); if (next_flood_advert && millisHasNowPassed(next_flood_advert)) { mesh::Packet *pkt = createSelfAdvert(); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 0e6c5f7009..f55d1d4084 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -85,6 +85,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t last_millis; uint64_t uptime_millis; unsigned long next_local_advert, next_flood_advert; + unsigned long next_battery_alert_check; + unsigned long last_battery_alert_sent; + bool battery_alert_sent; bool _logging; NodePrefs _prefs; ClientACL acl; @@ -163,6 +166,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); + bool sendRepeatersFloodText(const char* text); + void checkBatteryAlert(); File openAppend(const char* fname); bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index db56c5585c..3611c05dfc 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -556,6 +556,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { sizeof(_prefs->direct_retry_cr7_snr_x4)); // 662 retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 663 + _prefs->battery_alert_enabled = 0; + size_t battery_alert_read = file.read((uint8_t *)&_prefs->battery_alert_enabled, + sizeof(_prefs->battery_alert_enabled)); // 664 // PowerSaving-only prefs stored radio_fem_rxgain at 291, before direct retry timing existed. if (radio_fem_rxgain_read != sizeof(_prefs->radio_fem_rxgain) && legacy_retry_attempts_read == sizeof(legacy_retry_attempts_or_radio_fem_rxgain) @@ -654,6 +657,11 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { } else { sanitizeDirectRetryCrThresholds(_prefs); } + if (battery_alert_read != sizeof(_prefs->battery_alert_enabled)) { + _prefs->battery_alert_enabled = 0; + } else { + _prefs->battery_alert_enabled = constrain(_prefs->battery_alert_enabled, 0, 1); + } file.close(); } @@ -739,7 +747,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 661 file.write((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 662 file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 663 - // next: 664 + file.write((uint8_t *)&_prefs->battery_alert_enabled, sizeof(_prefs->battery_alert_enabled)); // 664 + // next: 665 file.close(); } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 104a7150c9..4b7d910c99 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -132,6 +132,7 @@ struct NodePrefs { // persisted to file int8_t direct_retry_cr5_snr_x4; int8_t direct_retry_cr7_snr_x4; int8_t direct_retry_cr8_snr_x4; + uint8_t battery_alert_enabled; }; class CommonCLICallbacks { From c93d907e85d5dd86e3b015e58e36894523309f5d Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 22 May 2026 14:34:19 -0700 Subject: [PATCH 50/94] Make battery alert thresholds configurable --- docs/cli_commands.md | 12 +++++- examples/simple_repeater/MyMesh.cpp | 62 +++++++++++++++++++++++++++-- src/helpers/CommonCLI.cpp | 20 +++++++++- src/helpers/CommonCLI.h | 2 + 4 files changed, 88 insertions(+), 8 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index e8d7a20a68..a5a55e803f 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -131,15 +131,23 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Usage:** - `get battery.alert` - `set battery.alert ` +- `get battery.alert.low` +- `set battery.alert.low ` +- `get battery.alert.critical` +- `set battery.alert.critical ` **Parameters:** - `state`: `on` (enable) or `off` (disable) +- `percent`: Battery percentage threshold **Default:** `off` +**Default thresholds:** `20` for `battery.alert.low`, `10` for `battery.alert.critical` + **Notes:** -- When enabled, sends a `#repeaters` flood text warning if voltage is above `1 V` and the battery estimate is below `20%`. -- Warnings repeat every `24` hours, or every `12` hours below `10%`. +- When enabled, sends a `#repeaters` flood text warning if voltage is above `1 V` and the battery estimate is below `battery.alert.low`. +- Warnings repeat every `24` hours, or every `12` hours below `battery.alert.critical`. +- `battery.alert.critical` must be lower than `battery.alert.low`. --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 6dc8bb7d74..2286251834 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -74,8 +74,8 @@ #endif #define LOW_BATTERY_MIN_VALID_MV 1000 -#define LOW_BATTERY_WARN_PERCENT 20 -#define LOW_BATTERY_CRITICAL_PERCENT 10 +#define LOW_BATTERY_WARN_PERCENT_DEFAULT 20 +#define LOW_BATTERY_CRITICAL_PERCENT_DEFAULT 10 #define LOW_BATTERY_CHECK_INTERVAL (60UL * 1000UL) #define LOW_BATTERY_WARN_INTERVAL (24UL * 60UL * 60UL * 1000UL) #define LOW_BATTERY_CRITICAL_INTERVAL (12UL * 60UL * 60UL * 1000UL) @@ -584,6 +584,30 @@ static uint8_t batteryPercentFromMilliVolts(uint16_t batt_mv) { return (uint8_t)pct; } +static bool parseBatteryAlertPercent(const char* value, uint8_t min_value, uint8_t max_value, uint8_t& result) { + if (value == NULL || *value == 0) { + return false; + } + + uint16_t parsed = 0; + while (*value) { + if (*value < '0' || *value > '9') { + return false; + } + parsed = (uint16_t)(parsed * 10 + (*value - '0')); + if (parsed > max_value) { + return false; + } + value++; + } + if (parsed < min_value) { + return false; + } + + result = (uint8_t)parsed; + return true; +} + mesh::Packet* MyMesh::createPacketCopy(const mesh::Packet* packet, const char* caller) { if (packet == NULL) return NULL; @@ -2024,6 +2048,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_retry_bridge_enabled = 0; _prefs.flood_retry_advert_enabled = 0; _prefs.battery_alert_enabled = 0; + _prefs.battery_alert_low_percent = LOW_BATTERY_WARN_PERCENT_DEFAULT; + _prefs.battery_alert_critical_percent = LOW_BATTERY_CRITICAL_PERCENT_DEFAULT; _prefs.direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT; _prefs.direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; _prefs.direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; @@ -2198,12 +2224,12 @@ void MyMesh::checkBatteryAlert() { uint16_t batt_mv = board.getBattMilliVolts(); uint8_t batt_pct = batteryPercentFromMilliVolts(batt_mv); - if (batt_mv <= LOW_BATTERY_MIN_VALID_MV || batt_pct >= LOW_BATTERY_WARN_PERCENT) { + if (batt_mv <= LOW_BATTERY_MIN_VALID_MV || batt_pct >= _prefs.battery_alert_low_percent) { battery_alert_sent = false; return; } - unsigned long interval = batt_pct < LOW_BATTERY_CRITICAL_PERCENT + unsigned long interval = batt_pct < _prefs.battery_alert_critical_percent ? LOW_BATTERY_CRITICAL_INTERVAL : LOW_BATTERY_WARN_INTERVAL; if (battery_alert_sent && !millisHasNowPassed(last_battery_alert_sent + interval)) { @@ -2630,6 +2656,10 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char * } } else if (strcmp(command, "get battery.alert") == 0) { sprintf(reply, "> %s", _prefs.battery_alert_enabled ? "on" : "off"); + } else if (strcmp(command, "get battery.alert.low") == 0) { + sprintf(reply, "> %u", (uint32_t)_prefs.battery_alert_low_percent); + } else if (strcmp(command, "get battery.alert.critical") == 0) { + sprintf(reply, "> %u", (uint32_t)_prefs.battery_alert_critical_percent); } else if (strncmp(command, "set battery.alert ", 18) == 0) { const char* value = command + 18; if (strcmp(value, "on") == 0) { @@ -2645,6 +2675,30 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char * } else { strcpy(reply, "Err - usage: set battery.alert "); } + } else if (strncmp(command, "set battery.alert.low ", 22) == 0) { + uint8_t percent; + if (!parseBatteryAlertPercent(command + 22, 1, 100, percent)) { + strcpy(reply, "Err - usage: set battery.alert.low <1-100>"); + } else if (percent <= _prefs.battery_alert_critical_percent) { + strcpy(reply, "Err - low must be greater than critical"); + } else { + _prefs.battery_alert_low_percent = percent; + next_battery_alert_check = 0; + savePrefs(); + strcpy(reply, "OK"); + } + } else if (strncmp(command, "set battery.alert.critical ", 27) == 0) { + uint8_t percent; + if (!parseBatteryAlertPercent(command + 27, 0, 99, percent)) { + strcpy(reply, "Err - usage: set battery.alert.critical <0-99>"); + } else if (percent >= _prefs.battery_alert_low_percent) { + strcpy(reply, "Err - critical must be less than low"); + } else { + _prefs.battery_alert_critical_percent = percent; + next_battery_alert_check = 0; + savePrefs(); + strcpy(reply, "OK"); + } } else if (strncmp(command, "get recent.repeater", 19) == 0 || strncmp(command, "set recent.repeater", 19) == 0 || strncmp(command, "clear recent.repeater", 21) == 0 diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 3611c05dfc..7e318c78eb 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -559,6 +559,12 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->battery_alert_enabled = 0; size_t battery_alert_read = file.read((uint8_t *)&_prefs->battery_alert_enabled, sizeof(_prefs->battery_alert_enabled)); // 664 + _prefs->battery_alert_low_percent = 20; + _prefs->battery_alert_critical_percent = 10; + size_t battery_alert_low_read = file.read((uint8_t *)&_prefs->battery_alert_low_percent, + sizeof(_prefs->battery_alert_low_percent)); // 665 + size_t battery_alert_critical_read = file.read((uint8_t *)&_prefs->battery_alert_critical_percent, + sizeof(_prefs->battery_alert_critical_percent)); // 666 // PowerSaving-only prefs stored radio_fem_rxgain at 291, before direct retry timing existed. if (radio_fem_rxgain_read != sizeof(_prefs->radio_fem_rxgain) && legacy_retry_attempts_read == sizeof(legacy_retry_attempts_or_radio_fem_rxgain) @@ -566,7 +572,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1)) { _prefs->radio_fem_rxgain = constrain(legacy_retry_attempts_or_radio_fem_rxgain, 0, 1); } - // next: 664 + // next: 667 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -662,6 +668,14 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { } else { _prefs->battery_alert_enabled = constrain(_prefs->battery_alert_enabled, 0, 1); } + if (battery_alert_low_read != sizeof(_prefs->battery_alert_low_percent) + || battery_alert_critical_read != sizeof(_prefs->battery_alert_critical_percent) + || _prefs->battery_alert_low_percent < 1 + || _prefs->battery_alert_low_percent > 100 + || _prefs->battery_alert_critical_percent >= _prefs->battery_alert_low_percent) { + _prefs->battery_alert_low_percent = 20; + _prefs->battery_alert_critical_percent = 10; + } file.close(); } @@ -748,7 +762,9 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 662 file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 663 file.write((uint8_t *)&_prefs->battery_alert_enabled, sizeof(_prefs->battery_alert_enabled)); // 664 - // next: 665 + file.write((uint8_t *)&_prefs->battery_alert_low_percent, sizeof(_prefs->battery_alert_low_percent)); // 665 + file.write((uint8_t *)&_prefs->battery_alert_critical_percent, sizeof(_prefs->battery_alert_critical_percent)); // 666 + // next: 667 file.close(); } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 4b7d910c99..112c41c078 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -133,6 +133,8 @@ struct NodePrefs { // persisted to file int8_t direct_retry_cr7_snr_x4; int8_t direct_retry_cr8_snr_x4; uint8_t battery_alert_enabled; + uint8_t battery_alert_low_percent; + uint8_t battery_alert_critical_percent; }; class CommonCLICallbacks { From c3712737e94267334e0f55df71c11693f2cf48c3 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 12 May 2026 23:50:47 -0700 Subject: [PATCH 51/94] build.sh: add optional Cascadia profile overrides --- build.sh | 329 +++++++++++++++++++++++++++- examples/simple_repeater/MyMesh.cpp | 46 +++- 2 files changed, 368 insertions(+), 7 deletions(-) diff --git a/build.sh b/build.sh index 9ef20d11ac..ad8f4079f3 100755 --- a/build.sh +++ b/build.sh @@ -4,6 +4,16 @@ ALL_PIO_ENVS=() PIO_CONFIG_JSON="" MENU_CHOICE="" SELECTED_TARGET="" +SELECTED_COMMAND_ARGS=() +MESHDEBUG_OVERRIDE="" +PACKET_LOGGING_OVERRIDE="" +RADIO_SETTINGS_API_URL="https://api.meshcore.nz/api/v1/config" +RADIO_SETTING_TITLE="" +RADIO_FREQ_OVERRIDE="" +RADIO_BW_OVERRIDE="" +RADIO_SF_OVERRIDE="" +RADIO_CR_OVERRIDE="" +FIRMWARE_PROFILE_OVERRIDE="" ENV_VARIANT_SUFFIX_PATTERN='companion_radio_serial|companion_radio_wifi|companion_radio_usb|comp_radio_usb|companion_usb|companion_radio_ble|companion_ble|repeater_bridge_rs232_serial1|repeater_bridge_rs232_serial2|repeater_bridge_rs232|repeater_bridge_espnow|terminal_chat|room_server|room_svr|kiss_modem|sensor|repeatr|repeater' BOARD_MODIFIER_WITHOUT_DISPLAY="_without_display" @@ -51,7 +61,7 @@ Examples: Build firmware for the "RAK_4631_repeater" device target $ bash build.sh build-firmware RAK_4631_repeater -Run without arguments to choose a target from an interactive menu +Run without arguments to choose an interactive build action/target, debug options, and radio settings $ bash build.sh Build all firmwares for device targets containing the string "RAK_4631" @@ -187,6 +197,277 @@ prompt_menu_choice() { done } +prompt_on_off_choice() { + local prompt_label=$1 + local default_choice=$2 + local choice + + while true; do + read -r -p "${prompt_label} [on/off] (default: ${default_choice}): " choice + choice=${choice,,} + if [ -z "$choice" ]; then + choice=$default_choice + fi + + case "$choice" in + on|off) + MENU_CHOICE="$choice" + return 0 + ;; + *) + echo "Invalid selection. Choose 'on' or 'off'." + ;; + esac + done +} + +prompt_for_build_mode() { + local options=( + "Build one firmware target" + "Build all firmwares" + "Build all repeater firmwares" + "Build all companion firmwares" + "Build all chat room server firmwares" + ) + + echo "No command provided. Select a build action:" + while true; do + print_numbered_menu "${options[@]}" + prompt_menu_choice "Build action" "${#options[@]}" + if [ "$MENU_CHOICE" == "QUIT" ]; then + echo "Cancelled." + exit 1 + fi + + case "$MENU_CHOICE" in + 1) + prompt_for_board_target + SELECTED_COMMAND_ARGS=(build-firmware "$SELECTED_TARGET") + return 0 + ;; + 2) + SELECTED_COMMAND_ARGS=(build-firmwares) + return 0 + ;; + 3) + SELECTED_COMMAND_ARGS=(build-repeater-firmwares) + return 0 + ;; + 4) + SELECTED_COMMAND_ARGS=(build-companion-firmwares) + return 0 + ;; + 5) + SELECTED_COMMAND_ARGS=(build-room-server-firmwares) + return 0 + ;; + esac + done +} + +prompt_for_debug_build_settings() { + echo "Set debug build options:" + prompt_on_off_choice "Mesh debug (MESH_DEBUG)" "off" + MESHDEBUG_OVERRIDE="$MENU_CHOICE" + + prompt_on_off_choice "Packet logging (MESH_PACKET_LOGGING)" "off" + PACKET_LOGGING_OVERRIDE="$MENU_CHOICE" + + echo "Using debug options: meshdebug=${MESHDEBUG_OVERRIDE}, packet_logging=${PACKET_LOGGING_OVERRIDE}" +} + +clear_radio_overrides() { + RADIO_SETTING_TITLE="" + RADIO_FREQ_OVERRIDE="" + RADIO_BW_OVERRIDE="" + RADIO_SF_OVERRIDE="" + RADIO_CR_OVERRIDE="" +} + +clear_firmware_profile_overrides() { + FIRMWARE_PROFILE_OVERRIDE="" +} + +set_radio_overrides() { + RADIO_SETTING_TITLE=$1 + RADIO_FREQ_OVERRIDE=$2 + RADIO_BW_OVERRIDE=$3 + RADIO_SF_OVERRIDE=$4 + RADIO_CR_OVERRIDE=$5 +} + +fetch_suggested_radio_settings() { + python3 - "$RADIO_SETTINGS_API_URL" <<'PY' +import json +import sys +import urllib.request + +url = sys.argv[1] +with urllib.request.urlopen(url, timeout=8) as response: + payload = json.load(response) + +entries = ( + payload.get("config", {}) + .get("suggested_radio_settings", {}) + .get("entries", []) +) + +for entry in entries: + title = str(entry.get("title", "")).strip() + description = str(entry.get("description", "")).strip() + freq = str(entry.get("frequency", "")).strip() + sf = str(entry.get("spreading_factor", "")).strip() + bw = str(entry.get("bandwidth", "")).strip() + cr = str(entry.get("coding_rate", "")).strip() + if title and freq and sf and bw and cr: + print("\t".join([title, description, freq, bw, sf, cr])) +PY +} + +is_valid_custom_radio_bandwidth() { + python3 - "$1" <<'PY' +import sys + +allowed = [7.81, 10.42, 15.63, 20.83, 31.25, 41.67, 62.5, 125.0, 250.0, 500.0] +try: + value = float(sys.argv[1]) +except Exception: + raise SystemExit(1) + +raise SystemExit(0 if any(abs(value - option) < 1e-6 for option in allowed) else 1) +PY +} + +prompt_for_custom_radio_setting() { + local freq + local sf + local bw + local cr + + echo + echo "Custom radio settings:" + + while true; do + read -r -p "Center frequency (MHz, e.g. 915.000): " freq + if [[ "$freq" =~ ^[0-9]+([.][0-9]+)?$ ]]; then + break + fi + echo "Please enter a numeric MHz value (e.g. 915.000)." + done + + echo "Spreading factor options: 5, 6, 7, 8, 9, 10, 11, 12" + while true; do + read -r -p "SF (5-12): " sf + if [[ "$sf" =~ ^[0-9]+$ ]] && [ "$sf" -ge 5 ] && [ "$sf" -le 12 ]; then + break + fi + echo "Please enter 5, 6, 7, 8, 9, 10, 11, or 12." + done + + echo "Bandwidth options (kHz): 7.81 10.42 15.63 20.83 31.25 41.67 62.5 125 250 500" + while true; do + read -r -p "BW (kHz): " bw + if [[ "$bw" =~ ^[0-9]+([.][0-9]+)?$ ]] && is_valid_custom_radio_bandwidth "$bw"; then + break + fi + echo "Please enter one of: 7.81 10.42 15.63 20.83 31.25 41.67 62.5 125 250 500." + done + + echo "Coding rate options: CR5, CR6, CR7, CR8" + while true; do + read -r -p "CR (5-8): " cr + if [[ "$cr" =~ ^[0-9]+$ ]] && [ "$cr" -ge 5 ] && [ "$cr" -le 8 ]; then + break + fi + echo "Please enter 5, 6, 7, or 8." + done + + set_radio_overrides "Custom" "$freq" "$bw" "$sf" "$cr" +} + +prompt_for_cascadia_profile_enable() { + clear_firmware_profile_overrides + echo + echo "Cascadia profile changes:" + echo " - rxdelay: 1" + echo " - agc.reset.interval: 8" + echo " - advert.interval: 0" + echo " - flood.advert.interval: 83" + echo " - multi.acks: 1" + echo " - path.hash.mode: 2" + echo " - loop.detect: minimal" + echo " - powersaving: on" + prompt_on_off_choice "Enable Cascadia profile overrides" "on" + if [ "$MENU_CHOICE" == "on" ]; then + FIRMWARE_PROFILE_OVERRIDE="cascadia" + echo "Using firmware profile override: ${FIRMWARE_PROFILE_OVERRIDE}" + return 0 + fi + + echo "Using target default firmware profile settings." + return 0 +} + +prompt_for_radio_build_settings() { + local -a preset_rows=() + local -a options=("Keep target defaults (no radio override)") + local row + local title + local description + local freq + local bw + local sf + local cr + local preset_index + local choice_index + local custom_index + + clear_radio_overrides + + if mapfile -t preset_rows < <(fetch_suggested_radio_settings); then + for row in "${preset_rows[@]}"; do + IFS=$'\t' read -r title description freq bw sf cr <<< "$row" + options+=("${title}: ${description}") + done + else + echo "Could not fetch radio presets from ${RADIO_SETTINGS_API_URL}." + preset_rows=() + fi + + options+=("Custom") + custom_index=${#options[@]} + + echo "Set radio build options:" + while true; do + print_numbered_menu "${options[@]}" + prompt_menu_choice "Radio setting" "${#options[@]}" + if [ "$MENU_CHOICE" == "QUIT" ]; then + echo "Cancelled." + exit 1 + fi + + choice_index=$MENU_CHOICE + if [ "$choice_index" -eq 1 ]; then + echo "Using target default radio settings." + return 0 + fi + + if [ "$choice_index" -eq "$custom_index" ]; then + prompt_for_custom_radio_setting + echo "Using radio setting: ${RADIO_SETTING_TITLE} (${RADIO_FREQ_OVERRIDE}MHz / SF${RADIO_SF_OVERRIDE} / BW${RADIO_BW_OVERRIDE} / CR${RADIO_CR_OVERRIDE})" + return 0 + fi + + preset_index=$((choice_index - 2)) + if [ "$preset_index" -ge 0 ] && [ "$preset_index" -lt "${#preset_rows[@]}" ]; then + IFS=$'\t' read -r title description freq bw sf cr <<< "${preset_rows[$preset_index]}" + set_radio_overrides "$title" "$freq" "$bw" "$sf" "$cr" + echo "Using radio setting: ${RADIO_SETTING_TITLE} (${RADIO_FREQ_OVERRIDE}MHz / SF${RADIO_SF_OVERRIDE} / BW${RADIO_BW_OVERRIDE} / CR${RADIO_CR_OVERRIDE})" + return 0 + fi + done +} + get_env_metadata() { local env_name=$1 local trimmed_env_name @@ -394,7 +675,7 @@ prompt_for_board_target() { mapfile -t boards < <(printf '%s\n' "${boards[@]}" | sort_lines_case_insensitive) - echo "No command provided. Select a board family:" + echo "Select a board family:" while true; do print_numbered_menu "${boards[@]}" prompt_menu_choice "Board selection" "${#boards[@]}" @@ -523,6 +804,40 @@ disable_debug_flags() { fi } +apply_debug_overrides() { + case "${MESHDEBUG_OVERRIDE,,}" in + on) + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_DEBUG -DMESH_DEBUG=1" + ;; + off) + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_DEBUG" + ;; + esac + + case "${PACKET_LOGGING_OVERRIDE,,}" in + on) + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_PACKET_LOGGING -DMESH_PACKET_LOGGING=1" + ;; + off) + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_PACKET_LOGGING" + ;; + esac +} + +apply_radio_overrides() { + if [ -n "$RADIO_FREQ_OVERRIDE" ] && [ -n "$RADIO_BW_OVERRIDE" ] && [ -n "$RADIO_SF_OVERRIDE" ] && [ -n "$RADIO_CR_OVERRIDE" ]; then + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -ULORA_FREQ -ULORA_BW -ULORA_SF -ULORA_CR -DLORA_FREQ=${RADIO_FREQ_OVERRIDE} -DLORA_BW=${RADIO_BW_OVERRIDE} -DLORA_SF=${RADIO_SF_OVERRIDE} -DLORA_CR=${RADIO_CR_OVERRIDE}" + fi +} + +apply_firmware_profile_overrides() { + case "${FIRMWARE_PROFILE_OVERRIDE,,}" in + cascadia) + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DCASCADIA_PROFILE=1 -DDEFAULT_RX_DELAY_BASE=1.0f -DDEFAULT_LOOP_DETECT=1 -DDEFAULT_POWERSAVING_ENABLED=1 -DDEFAULT_AGC_RESET_INTERVAL=2 -DDEFAULT_ADVERT_INTERVAL=0 -DDEFAULT_FLOOD_ADVERT_INTERVAL=83 -DDEFAULT_MULTI_ACKS=1 -DDEFAULT_PATH_HASH_MODE=2" + ;; + esac +} + copy_build_output() { local source_path=$1 local output_path=$2 @@ -616,6 +931,9 @@ build_firmware() { export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DFIRMWARE_BUILD_DATE='\"${firmware_build_date}\"' -DFIRMWARE_VERSION='\"${firmware_version_string}\"'" disable_debug_flags + apply_debug_overrides + apply_radio_overrides + apply_firmware_profile_overrides pio run -e "$env_name" collect_build_artifacts "$env_name" "$env_platform" "$firmware_filename" @@ -734,8 +1052,11 @@ main() { init_project_context if [ $# -eq 0 ]; then - prompt_for_board_target - set -- build-firmware "$SELECTED_TARGET" + prompt_for_build_mode + prompt_for_debug_build_settings + prompt_for_radio_build_settings + prompt_for_cascadia_profile_enable + set -- "${SELECTED_COMMAND_ARGS[@]}" fi prepare_output_dir diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 2286251834..26534931f5 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -20,6 +20,31 @@ #define LORA_TX_POWER 20 #endif +#ifndef DEFAULT_RX_DELAY_BASE + #define DEFAULT_RX_DELAY_BASE 0.0f +#endif +#ifndef DEFAULT_LOOP_DETECT + #define DEFAULT_LOOP_DETECT LOOP_DETECT_OFF +#endif +#ifndef DEFAULT_POWERSAVING_ENABLED + #define DEFAULT_POWERSAVING_ENABLED 0 +#endif +#ifndef DEFAULT_AGC_RESET_INTERVAL + #define DEFAULT_AGC_RESET_INTERVAL 0 +#endif +#ifndef DEFAULT_ADVERT_INTERVAL + #define DEFAULT_ADVERT_INTERVAL 1 +#endif +#ifndef DEFAULT_FLOOD_ADVERT_INTERVAL + #define DEFAULT_FLOOD_ADVERT_INTERVAL 12 +#endif +#ifndef DEFAULT_MULTI_ACKS + #define DEFAULT_MULTI_ACKS 0 +#endif +#ifndef DEFAULT_PATH_HASH_MODE + #define DEFAULT_PATH_HASH_MODE 0 +#endif + #ifndef ADVERT_NAME #define ADVERT_NAME "repeater" #endif @@ -2034,7 +2059,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc // defaults memset(&_prefs, 0, sizeof(_prefs)); _prefs.airtime_factor = 1.0; - _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; + _prefs.rx_delay_base = DEFAULT_RX_DELAY_BASE; _prefs.tx_delay_factor = 0.5f; // was 0.25f _prefs.direct_tx_delay_factor = 0.3f; // was 0.2 _prefs.direct_retry_recent_enabled = 1; @@ -2063,10 +2088,15 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.bw = LORA_BW; _prefs.cr = LORA_CR; _prefs.tx_power_dbm = LORA_TX_POWER; - _prefs.advert_interval = 1; // default to 2 minutes for NEW installs - _prefs.flood_advert_interval = 12; // 12 hours + _prefs.advert_interval = DEFAULT_ADVERT_INTERVAL; + _prefs.flood_advert_interval = DEFAULT_FLOOD_ADVERT_INTERVAL; _prefs.flood_max = 64; _prefs.interference_threshold = 0; // disabled + _prefs.agc_reset_interval = DEFAULT_AGC_RESET_INTERVAL; + _prefs.multi_acks = DEFAULT_MULTI_ACKS; + _prefs.path_hash_mode = DEFAULT_PATH_HASH_MODE; + _prefs.loop_detect = DEFAULT_LOOP_DETECT; + _prefs.powersaving_enabled = DEFAULT_POWERSAVING_ENABLED; // bridge defaults _prefs.bridge_enabled = 1; // enabled @@ -2105,6 +2135,16 @@ void MyMesh::begin(FILESYSTEM *fs) { _fs = fs; // load persisted prefs _cli.loadPrefs(_fs); +#ifdef CASCADIA_PROFILE + _prefs.rx_delay_base = DEFAULT_RX_DELAY_BASE; + _prefs.agc_reset_interval = DEFAULT_AGC_RESET_INTERVAL; + _prefs.advert_interval = DEFAULT_ADVERT_INTERVAL; + _prefs.flood_advert_interval = DEFAULT_FLOOD_ADVERT_INTERVAL; + _prefs.multi_acks = DEFAULT_MULTI_ACKS; + _prefs.path_hash_mode = DEFAULT_PATH_HASH_MODE; + _prefs.loop_detect = DEFAULT_LOOP_DETECT; + _prefs.powersaving_enabled = DEFAULT_POWERSAVING_ENABLED; +#endif acl.load(_fs, self_id); // TODO: key_store.begin(); region_map.load(_fs); From e1ba591a316cfc3537e4fe07c401eb28e29310f2 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 12 May 2026 16:37:26 -0700 Subject: [PATCH 52/94] build.sh: retry radio preset API fetch with browser headers --- build.sh | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/build.sh b/build.sh index ad8f4079f3..f8330cb5db 100755 --- a/build.sh +++ b/build.sh @@ -300,11 +300,47 @@ fetch_suggested_radio_settings() { python3 - "$RADIO_SETTINGS_API_URL" <<'PY' import json import sys +import urllib.error import urllib.request url = sys.argv[1] -with urllib.request.urlopen(url, timeout=8) as response: - payload = json.load(response) +header_sets = [ + { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", + "Accept": "application/json,text/plain,*/*", + "Accept-Language": "en-US,en;q=0.9", + "Referer": "https://meshcore.nz/", + "Origin": "https://meshcore.nz", + }, + { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0", + "Accept": "application/json,text/plain,*/*", + "Accept-Language": "en-US,en;q=0.9", + "Referer": "https://meshcore.nz/", + "Origin": "https://meshcore.nz", + }, +] + +payload = None +errors = [] + +for index, headers in enumerate(header_sets, start=1): + req = urllib.request.Request(url, headers=headers) + try: + with urllib.request.urlopen(req, timeout=8) as response: + payload = json.load(response) + break + except urllib.error.HTTPError as exc: + errors.append(f"attempt {index}: HTTP {exc.code}") + continue + except Exception as exc: + errors.append(f"attempt {index}: {type(exc).__name__}") + continue + +if payload is None: + summary = "; ".join(errors) if errors else "unknown error" + print(f"Failed to fetch radio presets from {url} ({summary})", file=sys.stderr) + raise SystemExit(1) entries = ( payload.get("config", {}) From f47b0872ac4961481d522fa28b58fca57d140687 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 12 May 2026 16:45:44 -0700 Subject: [PATCH 53/94] build.sh: fix radio override flags for PlatformIO ordering --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sh b/build.sh index f8330cb5db..2cddb1342e 100755 --- a/build.sh +++ b/build.sh @@ -862,7 +862,7 @@ apply_debug_overrides() { apply_radio_overrides() { if [ -n "$RADIO_FREQ_OVERRIDE" ] && [ -n "$RADIO_BW_OVERRIDE" ] && [ -n "$RADIO_SF_OVERRIDE" ] && [ -n "$RADIO_CR_OVERRIDE" ]; then - export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -ULORA_FREQ -ULORA_BW -ULORA_SF -ULORA_CR -DLORA_FREQ=${RADIO_FREQ_OVERRIDE} -DLORA_BW=${RADIO_BW_OVERRIDE} -DLORA_SF=${RADIO_SF_OVERRIDE} -DLORA_CR=${RADIO_CR_OVERRIDE}" + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DLORA_FREQ=${RADIO_FREQ_OVERRIDE} -DLORA_BW=${RADIO_BW_OVERRIDE} -DLORA_SF=${RADIO_SF_OVERRIDE} -DLORA_CR=${RADIO_CR_OVERRIDE}" fi } From 6d7dfa6814b7f566010dcfe5aae14c6db47f058d Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 12 May 2026 17:33:13 -0700 Subject: [PATCH 54/94] build.sh: add interactive Wi-Fi build settings overrides --- build.sh | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 2cddb1342e..dd471bab45 100755 --- a/build.sh +++ b/build.sh @@ -14,6 +14,9 @@ RADIO_BW_OVERRIDE="" RADIO_SF_OVERRIDE="" RADIO_CR_OVERRIDE="" FIRMWARE_PROFILE_OVERRIDE="" +WIFI_SSID_OVERRIDE="" +WIFI_PWD_OVERRIDE="" +WIFI_DEBUG_LOGGING_OVERRIDE="" ENV_VARIANT_SUFFIX_PATTERN='companion_radio_serial|companion_radio_wifi|companion_radio_usb|comp_radio_usb|companion_usb|companion_radio_ble|companion_ble|repeater_bridge_rs232_serial1|repeater_bridge_rs232_serial2|repeater_bridge_rs232|repeater_bridge_espnow|terminal_chat|room_server|room_svr|kiss_modem|sensor|repeatr|repeater' BOARD_MODIFIER_WITHOUT_DISPLAY="_without_display" @@ -61,7 +64,7 @@ Examples: Build firmware for the "RAK_4631_repeater" device target $ bash build.sh build-firmware RAK_4631_repeater -Run without arguments to choose an interactive build action/target, debug options, and radio settings +Run without arguments to choose an interactive build action/target, debug options, radio settings, and (when applicable) Wi-Fi settings $ bash build.sh Build all firmwares for device targets containing the string "RAK_4631" @@ -504,6 +507,81 @@ prompt_for_radio_build_settings() { done } +clear_wifi_overrides() { + WIFI_SSID_OVERRIDE="" + WIFI_PWD_OVERRIDE="" + WIFI_DEBUG_LOGGING_OVERRIDE="" +} + +is_wifi_build_target() { + local env_name=$1 + local is_wifi=1 + + shopt -s nocasematch + if [[ "$env_name" == *companion_radio_wifi* ]]; then + is_wifi=0 + fi + shopt -u nocasematch + + return "$is_wifi" +} + +selected_command_uses_wifi_target() { + case "${SELECTED_COMMAND_ARGS[0]:-}" in + build-firmware) + is_wifi_build_target "${SELECTED_COMMAND_ARGS[1]:-}" + return $? + ;; + *) + return 1 + ;; + esac +} + +prompt_for_wifi_build_settings() { + local -a options=( + "Keep target defaults (no Wi-Fi override)" + "Custom Wi-Fi SSID/password" + ) + local choice_index + + clear_wifi_overrides + + echo "Set Wi-Fi build options:" + while true; do + print_numbered_menu "${options[@]}" + prompt_menu_choice "Wi-Fi setting" "${#options[@]}" + if [ "$MENU_CHOICE" == "QUIT" ]; then + echo "Cancelled." + exit 1 + fi + + choice_index=$MENU_CHOICE + case "$choice_index" in + 1) + echo "Using target default Wi-Fi settings." + return 0 + ;; + 2) + read -r -p "Wi-Fi SSID: " WIFI_SSID_OVERRIDE + read -r -p "Wi-Fi password (blank allowed): " WIFI_PWD_OVERRIDE + prompt_on_off_choice "Wi-Fi debug logging (WIFI_DEBUG_LOGGING)" "off" + WIFI_DEBUG_LOGGING_OVERRIDE="$MENU_CHOICE" + echo "Using Wi-Fi overrides: ssid='${WIFI_SSID_OVERRIDE}', wifi_debug=${WIFI_DEBUG_LOGGING_OVERRIDE}" + return 0 + ;; + esac + done +} + +escape_cpp_string_literal() { + local value=$1 + + value=${value//\\/\\\\} + value=${value//\"/\\\"} + printf '%s' "$value" +} + get_env_metadata() { local env_name=$1 local trimmed_env_name @@ -874,6 +952,37 @@ apply_firmware_profile_overrides() { esac } +apply_wifi_overrides() { + local env_name=$1 + local ssid_escaped + local pwd_escaped + + if ! is_wifi_build_target "$env_name"; then + return 0 + fi + + if [ -n "$WIFI_SSID_OVERRIDE" ]; then + ssid_escaped=$(escape_cpp_string_literal "$WIFI_SSID_OVERRIDE") + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -D WIFI_SSID='\"${ssid_escaped}\"'" + fi + + if [ -n "$WIFI_SSID_OVERRIDE" ] || [ -n "$WIFI_PWD_OVERRIDE" ]; then + pwd_escaped=$(escape_cpp_string_literal "$WIFI_PWD_OVERRIDE") + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -D WIFI_PWD='\"${pwd_escaped}\"'" + fi + + case "${WIFI_DEBUG_LOGGING_OVERRIDE,,}" in + on) + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -D WIFI_DEBUG_LOGGING=1" + ;; + off) + if [ -n "$WIFI_SSID_OVERRIDE" ] || [ -n "$WIFI_PWD_OVERRIDE" ]; then + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -D WIFI_DEBUG_LOGGING=0" + fi + ;; + esac +} + copy_build_output() { local source_path=$1 local output_path=$2 @@ -970,6 +1079,7 @@ build_firmware() { apply_debug_overrides apply_radio_overrides apply_firmware_profile_overrides + apply_wifi_overrides "$env_name" pio run -e "$env_name" collect_build_artifacts "$env_name" "$env_platform" "$firmware_filename" @@ -1092,6 +1202,11 @@ main() { prompt_for_debug_build_settings prompt_for_radio_build_settings prompt_for_cascadia_profile_enable + if selected_command_uses_wifi_target; then + prompt_for_wifi_build_settings + else + clear_wifi_overrides + fi set -- "${SELECTED_COMMAND_ARGS[@]}" fi From 815bd2c1c3784adf4ef22bed9938f8c63a1c7ab8 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 12 May 2026 22:42:18 -0700 Subject: [PATCH 55/94] build.sh: accept 1/0 for on/off prompts --- build.sh | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/build.sh b/build.sh index dd471bab45..8ec35dae05 100755 --- a/build.sh +++ b/build.sh @@ -203,22 +203,33 @@ prompt_menu_choice() { prompt_on_off_choice() { local prompt_label=$1 local default_choice=$2 + local normalized_default local choice + normalized_default=${default_choice,,} + case "$normalized_default" in + 1) normalized_default="on" ;; + 0) normalized_default="off" ;; + esac + while true; do - read -r -p "${prompt_label} [on/off] (default: ${default_choice}): " choice + read -r -p "${prompt_label} [on/off/1/0] (default: ${normalized_default}): " choice choice=${choice,,} if [ -z "$choice" ]; then - choice=$default_choice + choice=$normalized_default fi case "$choice" in - on|off) - MENU_CHOICE="$choice" + on|1) + MENU_CHOICE="on" + return 0 + ;; + off|0) + MENU_CHOICE="off" return 0 ;; *) - echo "Invalid selection. Choose 'on' or 'off'." + echo "Invalid selection. Choose 'on'/'off' or 1/0." ;; esac done From 1ded5b34e6f74a4742e52453bb75fe562ed65b95 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 12 May 2026 22:43:50 -0700 Subject: [PATCH 56/94] build.sh: show on-off prompts as on(1)/off(0) --- build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sh b/build.sh index 8ec35dae05..f30af6da14 100755 --- a/build.sh +++ b/build.sh @@ -213,7 +213,7 @@ prompt_on_off_choice() { esac while true; do - read -r -p "${prompt_label} [on/off/1/0] (default: ${normalized_default}): " choice + read -r -p "${prompt_label} [on(1)/off(0)] (default: ${normalized_default}): " choice choice=${choice,,} if [ -z "$choice" ]; then choice=$normalized_default @@ -229,7 +229,7 @@ prompt_on_off_choice() { return 0 ;; *) - echo "Invalid selection. Choose 'on'/'off' or 1/0." + echo "Invalid selection. Choose on(1) or off(0)." ;; esac done From 0b245d51bff4562f11d35524301cb4fe211f55dc Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 22 May 2026 15:12:41 -0700 Subject: [PATCH 57/94] Update Halo Keymind settings docs --- docs/halo_keymind_settings.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/halo_keymind_settings.md b/docs/halo_keymind_settings.md index 1be3ddb0f8..bcd31c15e9 100644 --- a/docs/halo_keymind_settings.md +++ b/docs/halo_keymind_settings.md @@ -75,7 +75,6 @@ set flood.retry.ignore none | Setting | What it does | How to use | Example | | --- | --- | --- | --- | | `recent.repeater` | Shows or seeds the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. | `get recent.repeater`, `get recent.repeater `, `set recent.repeater ` | `set recent.repeater A1B2C3 -8.5` | -| `radio.fem.rxgain` | Controls the external LoRa FEM receive-path LNA where the board supports it. | `get radio.fem.rxgain`, `set radio.fem.rxgain on/off` | `set radio.fem.rxgain on` | ## Recent Repeater Table From bf389b34a13970e551ff5d45414801fb4fd388fa Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 22 May 2026 15:15:36 -0700 Subject: [PATCH 58/94] Document Keymind branch settings --- docs/halo_keymind_settings.md | 90 +++++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 10 deletions(-) diff --git a/docs/halo_keymind_settings.md b/docs/halo_keymind_settings.md index bcd31c15e9..72fd0db7d1 100644 --- a/docs/halo_keymind_settings.md +++ b/docs/halo_keymind_settings.md @@ -1,11 +1,10 @@ # Halo and Keymind Branch Settings -This file covers only CLI settings added by the Halo or Keymind branches. Use -`docs/cli_commands.md` for the general MeshCore CLI. +This file covers only CLI settings and helper commands added by the Halo or +Keymind branches. Use `docs/cli_commands.md` for the general MeshCore CLI. ## Quick Start - ```text set retry.preset rooftop set direct.retry.heard on @@ -25,7 +24,7 @@ get flood.retry.prefixes get flood.retry.ignore ``` -Use prefixes from the analyzer or neighbors list or `get recent.repeater` after the repeater has been online for a few hours. +Use prefixes from the analyzer or neighbors list or `get recent.repeater` after the repeater has been online for a few hours. ## Common Examples @@ -36,23 +35,23 @@ set flood.retry.advert off get flood.retry.advert ``` -Ignore a repeater as a successful flood retry echo: -Use this if you have a car repeater and a house repeater; have the house ignore the car. +Ignore a repeater as a successful flood retry echo: +Use this if you have a car repeater and a house repeater; have the house ignore the car. ```text set flood.retry.ignore 71CE82,C7618C get flood.retry.ignore ``` -Only accept specific downstream relays as flood retry success: -You're in a hole and need to hit a mountain top repeater to get out; keep trying till one you see one of these send out your packet. +Only accept specific downstream relays as flood retry success: +You're in a hole and need to hit a mountain top repeater to get out; keep trying till one you see one of these send out your packet. ```text set flood.retry.prefixes A58296,860CCA,425E5C get flood.retry.prefixes ``` -Bridge two groups of repeaters: +Bridge two groups of repeaters: ```text set flood.retry.bridge on @@ -74,7 +73,44 @@ set flood.retry.ignore none | Setting | What it does | How to use | Example | | --- | --- | --- | --- | -| `recent.repeater` | Shows or seeds the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. | `get recent.repeater`, `get recent.repeater `, `set recent.repeater ` | `set recent.repeater A1B2C3 -8.5` | +| `battery.alert` | Sends opt-in low-battery warnings to `#repeaters`. | `get battery.alert`, `set battery.alert on/off` | `set battery.alert on` | +| `battery.alert.low` | Warning threshold percentage. Must be greater than `battery.alert.critical`. | `get battery.alert.low`, `set battery.alert.low <1-100>` | `set battery.alert.low 20` | +| `battery.alert.critical` | Critical threshold percentage. Critical warnings repeat more often. | `get battery.alert.critical`, `set battery.alert.critical <0-99>` | `set battery.alert.critical 10` | +| `recent.repeater` | Shows, seeds, or clears the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. | `get recent.repeater`, `get recent.repeater `, `set recent.repeater `, `clear recent.repeater` | `set recent.repeater A1B2C3 -8.5` | +| `outpath` | Overrides the primary direct route used for replies to the current remote client. | `get outpath`, `set outpath `, `set outpath clear`, `set outpath flood` | `set outpath A1B2C3,D4E5F6` | +| `altpath` | Optional second direct route used for duplicate response attempts to the current remote client. | `get altpath`, `set altpath `, `set altpath clear` | `set altpath A1B2C3,D4E5F6` | + +## Other Keymind Commands + +| Command | What it does | How to use | Example | +| --- | --- | --- | --- | +| `send text.flood` | Sends a `#repeaters` flood text message formatted as `: `. | `send text.flood ` | `send text.flood checking ridge link` | + +## Battery Alerts + +Battery alerts are off by default. When enabled, the repeater checks once per +minute and sends a flood text warning to `#repeaters` when voltage is above +`1 V` and the estimated battery percent is below `battery.alert.low`. + +Warnings repeat every `24` hours, or every `12` hours when the estimate is +below `battery.alert.critical`. + +Defaults: + +| Setting | Default | +| --- | ---: | +| `battery.alert` | `off` | +| `battery.alert.low` | `20` | +| `battery.alert.critical` | `10` | + +Example: + +```text +set battery.alert.low 20 +set battery.alert.critical 10 +set battery.alert on +get battery.alert +``` ## Recent Repeater Table @@ -87,6 +123,7 @@ Show learned rows: ```text get recent.repeater get recent.repeater 2 +get recent.repeater page 3 ``` Seed or correct a prefix: @@ -95,9 +132,42 @@ Seed or correct a prefix: set recent.repeater A1B2C3 8.5 ``` +Clear learned and manually seeded rows: + +```text +clear recent.repeater +``` + Rows are sorted by prefix width, then SNR. A full direct retry failure lowers the matching row by `0.25 dB`. +Serial CLI pages contain up to `128` rows. Remote LoRa CLI pages contain up to +`7` rows. + +## Direct Path Overrides + +`outpath` and `altpath` apply to the current remote client ACL entry. They need +remote client context, so they are not useful from the local serial CLI. + +Set paths with comma-separated hop hashes. Each hop must be `2`, `4`, or `6` +hex characters, and all hops in one path must use the same width. + +```text +get outpath +set outpath A1B2C3,D4E5F6 +set outpath clear +set outpath flood + +get altpath +set altpath A1B2C3,D4E5F6 +set altpath clear +``` + +`set outpath clear` forgets the override and lets normal path discovery fill it +again. `set outpath flood` forces replies to use flood packets until the client +logs in again. `altpath` sends a duplicate reply over a second direct route; +clearing it returns replies to a single route. + ## Direct Retry Settings Direct retry applies to direct-routed packets. A queued resend is canceled when the next-hop echo is heard. From 0f0f91c9ffea4b24a005ce8a6c8435f60e37ba63 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Wed, 27 May 2026 09:41:29 +0700 Subject: [PATCH 59/94] Fixed directive and added isReadBusy for WiFi companion. --- examples/companion_radio/main.cpp | 2 +- src/helpers/esp32/SerialWifiInterface.cpp | 4 ++++ src/helpers/esp32/SerialWifiInterface.h | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index bb350fd047..a8daa0c803 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -265,7 +265,7 @@ void loop() { if (!the_mesh.hasPendingWork()) { #if defined(NRF52_PLATFORM) board.sleep(0); // nrf ignores seconds param, sleeps whenever possible -#else if defined(ESP32_PLATFORM) +#elif defined(ESP32_PLATFORM) if (!serial_interface.isReadBusy() && !serial_interface.isWriteBusy()) { // BLE is not busy vTaskDelay(pdMS_TO_TICKS(10)); // attempt to sleep } diff --git a/src/helpers/esp32/SerialWifiInterface.cpp b/src/helpers/esp32/SerialWifiInterface.cpp index 462e3ecc30..bdecb1a9dd 100644 --- a/src/helpers/esp32/SerialWifiInterface.cpp +++ b/src/helpers/esp32/SerialWifiInterface.cpp @@ -39,6 +39,10 @@ size_t SerialWifiInterface::writeFrame(const uint8_t src[], size_t len) { return 0; } +bool SerialWifiInterface::isReadBusy() const { + return false; +} + bool SerialWifiInterface::isWriteBusy() const { return false; } diff --git a/src/helpers/esp32/SerialWifiInterface.h b/src/helpers/esp32/SerialWifiInterface.h index 19291497fe..1ff1d83d25 100644 --- a/src/helpers/esp32/SerialWifiInterface.h +++ b/src/helpers/esp32/SerialWifiInterface.h @@ -52,6 +52,7 @@ class SerialWifiInterface : public BaseSerialInterface { bool isEnabled() const override { return _isEnabled; } bool isConnected() const override; + bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; From fec6ddbfa5f9db9354e183534c3576edb001cd98 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 28 May 2026 17:28:38 -0700 Subject: [PATCH 60/94] Remove Cascadia power saving default --- build.sh | 3 +-- docs/cli_commands.md | 2 +- examples/simple_repeater/MyMesh.cpp | 40 ++++++++++++++++++++++++++--- examples/simple_repeater/MyMesh.h | 2 ++ src/helpers/ESP32Board.h | 7 +++++ 5 files changed, 48 insertions(+), 6 deletions(-) diff --git a/build.sh b/build.sh index f30af6da14..f06d9109a4 100755 --- a/build.sh +++ b/build.sh @@ -446,7 +446,6 @@ prompt_for_cascadia_profile_enable() { echo " - multi.acks: 1" echo " - path.hash.mode: 2" echo " - loop.detect: minimal" - echo " - powersaving: on" prompt_on_off_choice "Enable Cascadia profile overrides" "on" if [ "$MENU_CHOICE" == "on" ]; then FIRMWARE_PROFILE_OVERRIDE="cascadia" @@ -958,7 +957,7 @@ apply_radio_overrides() { apply_firmware_profile_overrides() { case "${FIRMWARE_PROFILE_OVERRIDE,,}" in cascadia) - export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DCASCADIA_PROFILE=1 -DDEFAULT_RX_DELAY_BASE=1.0f -DDEFAULT_LOOP_DETECT=1 -DDEFAULT_POWERSAVING_ENABLED=1 -DDEFAULT_AGC_RESET_INTERVAL=2 -DDEFAULT_ADVERT_INTERVAL=0 -DDEFAULT_FLOOD_ADVERT_INTERVAL=83 -DDEFAULT_MULTI_ACKS=1 -DDEFAULT_PATH_HASH_MODE=2" + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DCASCADIA_PROFILE=1 -DDEFAULT_RX_DELAY_BASE=1.0f -DDEFAULT_LOOP_DETECT=1 -DDEFAULT_AGC_RESET_INTERVAL=2 -DDEFAULT_ADVERT_INTERVAL=0 -DDEFAULT_FLOOD_ADVERT_INTERVAL=83 -DDEFAULT_MULTI_ACKS=1 -DDEFAULT_PATH_HASH_MODE=2" ;; esac } diff --git a/docs/cli_commands.md b/docs/cli_commands.md index a5a55e803f..cfbbe25ada 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -498,7 +498,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `on`: enable power saving - `off`: disable power saving -**Default:** `on` +**Default:** `off` **Note:** When enabled, device enters sleep mode between radio transmissions diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 26534931f5..d6e8a0c133 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -2143,7 +2143,6 @@ void MyMesh::begin(FILESYSTEM *fs) { _prefs.multi_acks = DEFAULT_MULTI_ACKS; _prefs.path_hash_mode = DEFAULT_PATH_HASH_MODE; _prefs.loop_detect = DEFAULT_LOOP_DETECT; - _prefs.powersaving_enabled = DEFAULT_POWERSAVING_ENABLED; #endif acl.load(_fs, self_id); // TODO: key_store.begin(); @@ -2208,6 +2207,34 @@ void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint3 } } +void MyMesh::sendFloodScopedWithSelfPath(const TransportKey& scope, mesh::Packet* pkt, + uint32_t delay_millis, uint8_t path_hash_size) { + if (pkt == NULL) { + return; + } + if (path_hash_size == 0 || path_hash_size > MAX_ROUTE_HASH_BYTES) { + MESH_DEBUG_PRINTLN("%s MyMesh::sendFloodScopedWithSelfPath(): invalid path_hash_size", getLogDateTime()); + return; + } + + pkt->header &= ~PH_ROUTE_MASK; + if (scope.isNull()) { + pkt->header |= ROUTE_TYPE_FLOOD; + } else { + uint16_t codes[2]; + codes[0] = scope.calcTransportCode(pkt); + codes[1] = 0; + pkt->header |= ROUTE_TYPE_TRANSPORT_FLOOD; + pkt->transport_codes[0] = codes[0]; + pkt->transport_codes[1] = codes[1]; + } + + pkt->setPathHashSizeAndCount(path_hash_size, 1); + self_id.copyHashTo(pkt->path, path_hash_size); + getTables()->markSent(pkt); + sendPacket(pkt, 1, delay_millis); +} + bool MyMesh::sendRepeatersFloodText(const char* text) { if (text == NULL || *text == 0) return false; @@ -2223,8 +2250,15 @@ bool MyMesh::sendRepeatersFloodText(const char* text) { const size_t max_data_len = MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE; const size_t prefix_cap = max_data_len > 5 ? max_data_len - 5 + 1 : 0; + char sender_name[sizeof(_prefs.node_name)]; + StrHelper::strncpy(sender_name, _prefs.node_name, sizeof(sender_name)); + for (char* p = sender_name; *p; p++) { + if (*p == ':') { + *p = ';'; + } + } int prefix_written = prefix_cap > 0 - ? snprintf((char*)&temp[5], prefix_cap, "%s: ", _prefs.node_name) + ? snprintf((char*)&temp[5], prefix_cap, "%s: ", sender_name) : -1; if (prefix_written < 0) { return false; @@ -2247,7 +2281,7 @@ bool MyMesh::sendRepeatersFloodText(const char* text) { return false; } - sendFloodScoped(default_scope, pkt, 0, _prefs.path_hash_mode + 1); + sendFloodScopedWithSelfPath(default_scope, pkt, 0, _prefs.path_hash_mode + 1); return true; } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index f55d1d4084..5c1f09cb05 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -242,6 +242,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { const uint8_t* path, uint8_t path_len, const uint8_t* alt_path, uint8_t alt_path_len, uint32_t delay_millis); + void sendFloodScopedWithSelfPath(const TransportKey& scope, mesh::Packet* pkt, + uint32_t delay_millis, uint8_t path_hash_size); public: MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index fe9865931d..e99752d9ce 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -73,6 +73,13 @@ class ESP32Board : public mesh::MainBoard { return; } + #if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT + if (Serial) { + delay(1); + return; + } + #endif + // Set GPIO wakeup gpio_num_t wakeupPin = (gpio_num_t)getIRQGpio(); From 4f9b8bf1efd019f3b0d347fb25167001df456417 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 28 May 2026 17:33:33 -0700 Subject: [PATCH 61/94] Handle direct route path consumption --- src/Mesh.cpp | 110 +++++++++++++++++++++++++++++++++++++++++---------- src/Mesh.h | 3 +- 2 files changed, 91 insertions(+), 22 deletions(-) diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 8b088b3171..dbdbbf24dc 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -254,26 +254,35 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } } - if ((self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) || maybeShortCircuitDirect(pkt)) && allowPacketForward(pkt)) { - if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { - return forwardMultipartDirect(pkt); - } else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { - if (!_tables->hasSeen(pkt)) { // don't retransmit! - removeSelfFromPath(pkt); - routeDirectRecvAcks(pkt, 0); + if (canDecodeDirectPayloadForSelf(pkt)) { + // Some path sources include the final node hash, and some packets are + // heard before all planned hops are consumed. Only stop forwarding once + // this node proves it can decrypt the payload. + removePathPrefix(pkt, pkt->getPathHashCount()); + } else if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) || maybeShortCircuitDirect(pkt)) { + if (allowPacketForward(pkt)) { + if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { + return forwardMultipartDirect(pkt); + } else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { + if (!_tables->hasSeen(pkt)) { // don't retransmit! + removePathPrefix(pkt, 1); + routeDirectRecvAcks(pkt, 0); + } + return ACTION_RELEASE; } - return ACTION_RELEASE; - } - if (!_tables->hasSeen(pkt)) { - removeSelfFromPath(pkt); + if (!_tables->hasSeen(pkt)) { + removePathPrefix(pkt, 1); - uint32_t d = getDirectRetransmitDelay(pkt); - maybeScheduleDirectRetry(pkt, 0); - return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority + uint32_t d = getDirectRetransmitDelay(pkt); + maybeScheduleDirectRetry(pkt, 0); + return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority + } } } - return ACTION_RELEASE; // this node is NOT the next hop (OR this packet has already been forwarded), so discard. + if (pkt->getPathHashCount() > 0) { + return ACTION_RELEASE; // this node is NOT the next hop (OR this packet has already been forwarded), so discard. + } } if (pkt->isRouteFlood() && filterRecvFloodPacket(pkt)) return ACTION_RELEASE; @@ -487,13 +496,16 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { return action; } -void Mesh::removeSelfFromPath(Packet* pkt) { - // remove our hash from 'path' - pkt->setPathHashCount(pkt->getPathHashCount() - 1); // decrement the count +void Mesh::removePathPrefix(Packet* pkt, uint8_t prefix_count) { + uint8_t hash_count = pkt->getPathHashCount(); + if (prefix_count == 0 || hash_count == 0) return; + if (prefix_count > hash_count) prefix_count = hash_count; + pkt->setPathHashCount(hash_count - prefix_count); uint8_t sz = pkt->getPathHashSize(); - for (int k = 0; k < pkt->getPathHashCount()*sz; k += sz) { // shuffle path by 1 'entry' - memcpy(&pkt->path[k], &pkt->path[k + sz], sz); + uint8_t prefix_bytes = prefix_count * sz; + for (int k = 0; k < pkt->getPathHashCount()*sz; k += sz) { + memmove(&pkt->path[k], &pkt->path[k + prefix_bytes], sz); } } @@ -526,7 +538,7 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) { memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); if (!_tables->hasSeen(&tmp)) { // don't retransmit! - removeSelfFromPath(&tmp); + removePathPrefix(&tmp, 1); routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2) } } @@ -839,6 +851,62 @@ bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_h } } +bool Mesh::canDecodeDirectPayloadForSelf(const Packet* packet) { + if (packet == NULL || !packet->isRouteDirect() || packet->getPathHashCount() == 0 || packet->payload_len < 1) { + return false; + } + + switch (packet->getPayloadType()) { + case PAYLOAD_TYPE_PATH: + case PAYLOAD_TYPE_REQ: + case PAYLOAD_TYPE_RESPONSE: + case PAYLOAD_TYPE_TXT_MSG: { + if (packet->payload_len < 2) { + return false; + } + + int i = 0; + uint8_t dest_hash = packet->payload[i++]; + uint8_t src_hash = packet->payload[i++]; + if (i + CIPHER_MAC_SIZE >= packet->payload_len || !self_id.isHashMatch(&dest_hash)) { + return false; + } + + int num = searchPeersByHash(&src_hash); + for (int j = 0; j < num; j++) { + uint8_t secret[PUB_KEY_SIZE]; + getPeerSharedSecret(secret, j); + + uint8_t data[MAX_PACKET_PAYLOAD]; + if (Utils::MACThenDecrypt(secret, data, &packet->payload[i], packet->payload_len - i) > 0) { + return true; + } + } + return false; + } + + case PAYLOAD_TYPE_ANON_REQ: { + int i = 0; + uint8_t dest_hash = packet->payload[i++]; + if (i + PUB_KEY_SIZE + CIPHER_MAC_SIZE >= packet->payload_len || !self_id.isHashMatch(&dest_hash)) { + return false; + } + + Identity sender(&packet->payload[i]); + i += PUB_KEY_SIZE; + + uint8_t secret[PUB_KEY_SIZE]; + self_id.calcSharedSecret(secret, sender); + + uint8_t data[MAX_PACKET_PAYLOAD]; + return Utils::MACThenDecrypt(secret, data, &packet->payload[i], packet->payload_len - i) > 0; + } + + default: + return false; + } +} + void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { const uint8_t* next_hop_hash; uint8_t next_hop_hash_len; diff --git a/src/Mesh.h b/src/Mesh.h index 483d48260e..6ae26642fc 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -75,7 +75,7 @@ class Mesh : public Dispatcher { DirectRetryEntry _direct_retries[MAX_DIRECT_RETRY_SLOTS]; FloodRetryEntry _flood_retries[MAX_FLOOD_RETRY_SLOTS]; - void removeSelfFromPath(Packet* packet); + void removePathPrefix(Packet* packet, uint8_t prefix_count); void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis); void clearDirectRetrySlot(int idx); bool isDirectRetryQueued(const Packet* packet) const; @@ -85,6 +85,7 @@ class Mesh : public Dispatcher { void clearPendingDirectRetryOnSendFail(const Packet* packet); bool getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len, uint8_t& progress_marker, bool& expect_path_growth) const; + bool canDecodeDirectPayloadForSelf(const Packet* packet); void maybeScheduleDirectRetry(const Packet* packet, uint8_t priority); void clearFloodRetrySlot(int idx); bool isFloodRetryQueued(const Packet* packet) const; From 618c2b6773263e6e8c7c2d3a118834165c6a5d31 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 2 Jun 2026 16:22:12 -0700 Subject: [PATCH 62/94] Support direct outpath override --- docs/cli_commands.md | 2 ++ docs/halo_keymind_settings.md | 12 +++++++----- examples/simple_repeater/MyMesh.cpp | 10 +++++++++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index cfbbe25ada..d1cba8f963 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -943,6 +943,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Usage:** - `get outpath` - `set outpath ` +- `set outpath direct` - `set outpath clear` - `set outpath flood` - `get altpath` @@ -956,6 +957,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - These commands require remote client context; they target the caller's ACL entry. - The path hash size is inferred from the hop hash width. - `outpath` overrides the primary direct route used for replies to the caller. +- `direct` sets a zero-hop direct route for a caller reachable without repeaters. - `clear` forgets the current direct path and allows normal path discovery to repopulate it. - `flood` forces replies to use flood packets until the client logs in again. - `altpath` is an optional second direct route used for duplicate response attempts. diff --git a/docs/halo_keymind_settings.md b/docs/halo_keymind_settings.md index 72fd0db7d1..ca2c080329 100644 --- a/docs/halo_keymind_settings.md +++ b/docs/halo_keymind_settings.md @@ -77,7 +77,7 @@ set flood.retry.ignore none | `battery.alert.low` | Warning threshold percentage. Must be greater than `battery.alert.critical`. | `get battery.alert.low`, `set battery.alert.low <1-100>` | `set battery.alert.low 20` | | `battery.alert.critical` | Critical threshold percentage. Critical warnings repeat more often. | `get battery.alert.critical`, `set battery.alert.critical <0-99>` | `set battery.alert.critical 10` | | `recent.repeater` | Shows, seeds, or clears the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. | `get recent.repeater`, `get recent.repeater `, `set recent.repeater `, `clear recent.repeater` | `set recent.repeater A1B2C3 -8.5` | -| `outpath` | Overrides the primary direct route used for replies to the current remote client. | `get outpath`, `set outpath `, `set outpath clear`, `set outpath flood` | `set outpath A1B2C3,D4E5F6` | +| `outpath` | Overrides the primary direct route used for replies to the current remote client. | `get outpath`, `set outpath `, `set outpath direct`, `set outpath clear`, `set outpath flood` | `set outpath A1B2C3,D4E5F6` | | `altpath` | Optional second direct route used for duplicate response attempts to the current remote client. | `get altpath`, `set altpath `, `set altpath clear` | `set altpath A1B2C3,D4E5F6` | ## Other Keymind Commands @@ -155,6 +155,7 @@ hex characters, and all hops in one path must use the same width. ```text get outpath set outpath A1B2C3,D4E5F6 +set outpath direct set outpath clear set outpath flood @@ -163,10 +164,11 @@ set altpath A1B2C3,D4E5F6 set altpath clear ``` -`set outpath clear` forgets the override and lets normal path discovery fill it -again. `set outpath flood` forces replies to use flood packets until the client -logs in again. `altpath` sends a duplicate reply over a second direct route; -clearing it returns replies to a single route. +`set outpath direct` sets a zero-hop direct route for a client reachable without +repeaters. `set outpath clear` forgets the override and lets normal path +discovery fill it again. `set outpath flood` forces replies to use flood packets +until the client logs in again. `altpath` sends a duplicate reply over a second +direct route; clearing it returns replies to a single route. ## Direct Retry Settings diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index d6e8a0c133..46508c32f9 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -579,7 +579,7 @@ static bool pathsEqual(const uint8_t* a, uint8_t a_len, const uint8_t* b, uint8_ } static bool hasUsablePath(const uint8_t* path, uint8_t path_len) { - return path != NULL && mesh::Packet::isValidPathLen(path_len) && (path_len & 63) > 0; + return path != NULL && mesh::Packet::isValidPathLen(path_len); } static bool buildRepeatersChannel(mesh::GroupChannel& channel) { @@ -2529,6 +2529,10 @@ static bool parsePathCommand(char* raw, uint8_t* out_path, uint8_t& out_path_len out_path_len = OUT_PATH_FORCE_FLOOD; return true; } + if (strcmp(spec, "direct") == 0) { + out_path_len = 0; + return true; + } uint8_t hash_size = 0; uint8_t hop_count = 0; @@ -2586,6 +2590,10 @@ static void formatPathReply(const uint8_t* path, uint8_t path_len, char* out, si snprintf(out, out_len, "> invalid"); return; } + if ((path_len & 63) == 0) { + snprintf(out, out_len, "> direct"); + return; + } uint8_t hash_size = (path_len >> 6) + 1; uint8_t hop_count = path_len & 63; From 2af7b75d70dd64031398352e1d503e058059aa6b Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 21 Apr 2026 16:24:00 -0700 Subject: [PATCH 63/94] For packets with a path set; auto try again if no echo was heard --- docs/cli_commands.md | 28 +++ examples/simple_repeater/MyMesh.cpp | 90 +++++++++- examples/simple_repeater/MyMesh.h | 7 + src/Dispatcher.cpp | 8 +- src/Dispatcher.h | 2 + src/Mesh.cpp | 270 +++++++++++++++++++++++++++- src/Mesh.h | 41 +++++ src/helpers/CommonCLI.cpp | 55 +++++- src/helpers/CommonCLI.h | 4 +- src/helpers/SimpleMeshTables.h | 172 +++++++++++++++--- 10 files changed, 642 insertions(+), 35 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 6b4f61578b..c6a7fac01b 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -405,6 +405,34 @@ --- +#### View or change whether direct retries can fall back to the recently-heard repeater list +**Usage:** +- `get direct.retry.heard` +- `set direct.retry.heard ` + +**Parameters:** +- `state`: `on`|`off` + +**Default:** `off` + +**Note:** When enabled, a repeater can use recently-heard non-duplicate repeater prefixes as a fallback for direct retry eligibility when no suitable neighbor entry is available. + +--- + +#### View or change the SNR margin used for direct retry eligibility +**Usage:** +- `get direct.retry.margin` +- `set direct.retry.margin ` + +**Parameters:** +- `value`: Margin in dB above the SF-specific receive floor (minimum `0`, default `5`) + +**Default:** `5` + +**Note:** The retry gate uses the active SF floor of `SF5=-2.5`, `SF6=-5`, `SF7=-7.5`, `SF8=-10`, `SF9=-12.5`, `SF10=-15`, `SF11=-17.5`, `SF12=-20`, then adds this margin. + +--- + #### [Experimental] View or change the processing delay for received traffic **Usage:** - `get rxdelay` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 6d957cc094..f268679c64 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -40,6 +40,9 @@ #ifndef TXT_ACK_DELAY #define TXT_ACK_DELAY 200 #endif +#ifndef HALO_DIRECT_RETRY_DELAY_MIN + #define HALO_DIRECT_RETRY_DELAY_MIN 200 +#endif #define FIRMWARE_VER_LEVEL 2 @@ -60,6 +63,20 @@ #define LAZY_CONTACTS_WRITE_DELAY 5000 +const NeighbourInfo* MyMesh::findNeighbourByHash(const uint8_t* hash, uint8_t hash_len) const { +#if MAX_NEIGHBOURS + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + if (neighbours[i].heard_timestamp > 0 && neighbours[i].id.isHashMatch(hash, hash_len)) { + return &neighbours[i]; + } + } +#else + (void)hash; + (void)hash_len; +#endif + return NULL; +} + void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { #if MAX_NEIGHBOURS // check if neighbours enabled // find existing neighbour, else use least recently updated @@ -383,6 +400,25 @@ File MyMesh::openAppend(const char *fname) { #endif } +static uint8_t max_loop_minimal[] = { 0, /* 1-byte */ 4, /* 2-byte */ 2, /* 3-byte */ 1 }; +static uint8_t max_loop_moderate[] = { 0, /* 1-byte */ 2, /* 2-byte */ 1, /* 3-byte */ 1 }; +static uint8_t max_loop_strict[] = { 0, /* 1-byte */ 1, /* 2-byte */ 1, /* 3-byte */ 1 }; +// SF5..SF12 receive floors, scaled by 4 so we can keep the retry gate in int8_t quarter-dB units. +static const int8_t direct_retry_floor_x4[] = { -10, -20, -30, -40, -50, -60, -70, -80 }; + +bool MyMesh::isLooped(const mesh::Packet* packet, const uint8_t max_counters[]) { + uint8_t hash_size = packet->getPathHashSize(); + uint8_t hash_count = packet->getPathHashCount(); + uint8_t n = 0; + const uint8_t* path = packet->path; + while (hash_count > 0) { // count how many times this node is already in the path + if (self_id.isHashMatch(path, hash_size)) n++; + hash_count--; + path += hash_size; + } + return n >= max_counters[hash_size]; +} + bool MyMesh::allowPacketForward(const mesh::Packet *packet) { if (_prefs.disable_fwd) return false; if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false; @@ -487,6 +523,44 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } +int8_t MyMesh::getDirectRetryMinSNRX4() const { + // Use the live SF so `tempradio` changes immediately affect the retry threshold. + uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); + int16_t threshold = direct_retry_floor_x4[sf - 5] + ((int16_t)_prefs.direct_retry_snr_margin_db * 4); + return (int8_t)constrain(threshold, -128, 127); +} +bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { + if (_prefs.disable_fwd) { + return false; + } + + int8_t min_snr_x4 = getDirectRetryMinSNRX4(); + const NeighbourInfo* neighbour = findNeighbourByHash(next_hop_hash, next_hop_hash_len); + // Prefer the explicit neighbor table first; it is the strongest signal that this hop is still reachable. + if (neighbour != NULL && neighbour->snr >= min_snr_x4) { + return true; + } + + if (!_prefs.direct_retry_recent_enabled) { + return false; + } + + // If no neighbor entry exists, fall back to the recent-heard repeater cache keyed by the same path prefix. + const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len); + return recent != NULL && recent->snr_x4 >= min_snr_x4; +} +uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { + // Approximate LoRa line rate in kilobits/sec from the live radio params the repeater is using now. + float kbps = (((float) active_sf) * active_bw * ((float) active_cr)) / ((float) (1UL << active_sf)); + if (kbps <= 0.0f) { + return HALO_DIRECT_RETRY_DELAY_MIN; + } + + // Wait roughly long enough for our transmission, the next hop's receive/forward window, and its echo back. + uint32_t bits = ((uint32_t) packet->getRawLength()) * 8; + uint32_t scaled_wait_millis = (uint32_t) ((((float) bits) * 4.0f) / kbps); + return max((uint32_t) HALO_DIRECT_RETRY_DELAY_MIN, scaled_wait_millis); +} bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { // just try to determine region for packet (apply later in allowPacketForward()) @@ -771,7 +845,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.airtime_factor = 1.0; // one half _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.tx_delay_factor = 0.5f; // was 0.25f - _prefs.direct_tx_delay_factor = 0.2f; // was zero + _prefs.direct_tx_delay_factor = 0.3f; // was 0.2 + _prefs.direct_retry_recent_enabled = 0; + _prefs.direct_retry_snr_margin_db = 5; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; _prefs.node_lon = ADVERT_LON; @@ -801,6 +877,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.advert_loc_policy = ADVERT_LOC_PREFS; _prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier + active_bw = _prefs.bw; + active_sf = _prefs.sf; + active_cr = _prefs.cr; } void MyMesh::begin(FILESYSTEM *fs) { @@ -819,6 +898,9 @@ void MyMesh::begin(FILESYSTEM *fs) { #endif radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + active_bw = _prefs.bw; + active_sf = _prefs.sf; + active_cr = _prefs.cr; radio_set_tx_power(_prefs.tx_power_dbm); updateAdvertTimer(); @@ -1196,12 +1278,18 @@ void MyMesh::loop() { if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params set_radio_at = 0; // clear timer radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr); + active_bw = pending_bw; + active_sf = pending_sf; + active_cr = pending_cr; MESH_DEBUG_PRINTLN("Temp radio params"); } if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig revert_radio_at = 0; // clear timer radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + active_bw = _prefs.bw; + active_sf = _prefs.sf; + active_cr = _prefs.cr; MESH_DEBUG_PRINTLN("Radio params restored"); } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 0d5cd28a3d..315288296a 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -106,8 +106,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { unsigned long set_radio_at, revert_radio_at; float pending_freq; float pending_bw; + float active_bw; // live BW, including temporary radio overrides uint8_t pending_sf; + uint8_t active_sf; // live SF, including temporary radio overrides uint8_t pending_cr; + uint8_t active_cr; // live CR, including temporary radio overrides int matching_peer_indexes[MAX_CLIENTS]; #if defined(WITH_RS232_BRIDGE) RS232Bridge bridge; @@ -115,6 +118,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { ESPNowBridge bridge; #endif + const NeighbourInfo* findNeighbourByHash(const uint8_t* hash, uint8_t hash_len) const; + int8_t getDirectRetryMinSNRX4() const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); @@ -141,6 +146,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t getRetransmitDelay(const mesh::Packet* packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; + bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override; + uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override; int getInterferenceThreshold() const override { return _prefs.interference_threshold; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 0a1549851e..d9dd00a381 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -68,7 +68,8 @@ void Dispatcher::loop() { next_tx_time = futureMillis(t * getAirtimeBudgetFactor()); _radio->onSendFinished(); - logTx(outbound, 2 + outbound->path_len + outbound->payload_len); + logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); + onSendComplete(outbound); if (outbound->isRouteFlood()) { n_sent_flood++; } else { @@ -80,7 +81,8 @@ void Dispatcher::loop() { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime()); _radio->onSendFinished(); - logTxFail(outbound, 2 + outbound->path_len + outbound->payload_len); + logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); + onSendFail(outbound); releasePacket(outbound); // return to pool outbound = NULL; @@ -330,4 +332,4 @@ unsigned long Dispatcher::futureMillis(int millis_from_now) const { return _ms->getMillis() + millis_from_now; } -} \ No newline at end of file +} diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 25a41d82ce..29460061f4 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -151,6 +151,8 @@ class Dispatcher { virtual void logRx(Packet* packet, int len, float score) { } // hooks for custom logging virtual void logTx(Packet* packet, int len) { } virtual void logTxFail(Packet* packet, int len) { } + virtual void onSendComplete(Packet* packet) { } + virtual void onSendFail(Packet* packet) { } virtual const char* getLogDateTime() { return ""; } virtual float getAirtimeBudgetFactor() const; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 0548c9073d..082b6bb444 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -4,11 +4,32 @@ namespace mesh { void Mesh::begin() { + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + _direct_retries[i].packet = NULL; + _direct_retries[i].trigger_packet = NULL; + _direct_retries[i].retry_at = 0; + _direct_retries[i].retry_delay = 0; + _direct_retries[i].priority = 0; + _direct_retries[i].progress_marker = 0; + _direct_retries[i].expect_path_growth = false; + _direct_retries[i].queued = false; + _direct_retries[i].active = false; + } Dispatcher::begin(); } void Mesh::loop() { Dispatcher::loop(); + + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active || !_direct_retries[i].queued || !millisHasNowPassed(_direct_retries[i].retry_at)) { + continue; + } + + if (!isDirectRetryQueued(_direct_retries[i].packet)) { + clearDirectRetrySlot(i); + } + } } bool Mesh::allowPacketForward(const mesh::Packet* packet) { @@ -22,10 +43,25 @@ uint32_t Mesh::getRetransmitDelay(const mesh::Packet* packet) { uint32_t Mesh::getDirectRetransmitDelay(const Packet* packet) { return 0; // by default, no delay } +bool Mesh::allowDirectRetry(const Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { + return false; +} +uint32_t Mesh::getDirectRetryEchoDelay(const Packet* packet) const { + // Keep the base fallback aligned with the repeater's minimum retry wait. + return 200; +} uint8_t Mesh::getExtraAckTransmitCount() const { return 0; } +void Mesh::onSendComplete(Packet* packet) { + armDirectRetryOnSendComplete(packet); +} + +void Mesh::onSendFail(Packet* packet) { + clearPendingDirectRetryOnSendFail(packet); +} + uint32_t Mesh::getCADFailRetryDelay() const { return _rng->nextInt(1, 4)*120; } @@ -44,6 +80,10 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { return ACTION_RELEASE; } + if (pkt->isRouteDirect()) { + cancelDirectRetryOnEcho(pkt); + } + if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { if (pkt->path_len < MAX_PATH_SIZE) { uint8_t i = 0; @@ -63,6 +103,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { pkt->path[pkt->path_len++] = (int8_t) (pkt->getSNR()*4); uint32_t d = getDirectRetransmitDelay(pkt); + maybeScheduleDirectRetry(pkt, 5); return ACTION_RETRANSMIT_DELAYED(5, d); // schedule with priority 5 (for now), maybe make configurable? } } @@ -103,6 +144,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { removeSelfFromPath(pkt); uint32_t d = getDirectRetransmitDelay(pkt); + maybeScheduleDirectRetry(pkt, 0); return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority } } @@ -379,6 +421,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { memcpy(a1->path, packet->path, a1->path_len = packet->path_len); a1->header &= ~PH_ROUTE_MASK; a1->header |= ROUTE_TYPE_DIRECT; + maybeScheduleDirectRetry(a1, 0); sendPacket(a1, 0, delay_millis); } extra--; @@ -389,11 +432,225 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { memcpy(a2->path, packet->path, a2->path_len = packet->path_len); a2->header &= ~PH_ROUTE_MASK; a2->header |= ROUTE_TYPE_DIRECT; + maybeScheduleDirectRetry(a2, 0); sendPacket(a2, 0, delay_millis); } } } +void Mesh::clearDirectRetrySlot(int idx) { + _direct_retries[idx].packet = NULL; + _direct_retries[idx].trigger_packet = NULL; + _direct_retries[idx].retry_at = 0; + _direct_retries[idx].retry_delay = 0; + _direct_retries[idx].priority = 0; + _direct_retries[idx].progress_marker = 0; + _direct_retries[idx].expect_path_growth = false; + _direct_retries[idx].queued = false; + _direct_retries[idx].active = false; +} + +bool Mesh::isDirectRetryQueued(const Packet* packet) const { + for (int i = 0; i < _mgr->getOutboundTotal(); i++) { + if (_mgr->getOutboundByIdx(i) == packet) { + return true; + } + } + return false; +} + +void Mesh::calculateDirectRetryKey(const Packet* packet, uint8_t* dest_key) const { + uint8_t type = packet->getPayloadType(); + Utils::sha256(dest_key, MAX_HASH_SIZE, &type, 1, packet->payload, packet->payload_len); +} + +bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { + uint8_t recv_key[MAX_HASH_SIZE]; + calculateDirectRetryKey(packet, recv_key); + + bool cleared = false; + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active || memcmp(recv_key, _direct_retries[i].retry_key, MAX_HASH_SIZE) != 0) { + continue; + } + + bool is_echo = _direct_retries[i].expect_path_growth + ? packet->path_len > _direct_retries[i].progress_marker + : packet->getPathHashCount() < _direct_retries[i].progress_marker; + if (!is_echo) { + continue; + } + + if (_direct_retries[i].queued) { + for (int j = 0; j < _mgr->getOutboundTotal(); j++) { + if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { + Packet* pending = _mgr->removeOutboundByIdx(j); + if (pending) { + releasePacket(pending); + } + break; + } + } + clearDirectRetrySlot(i); + } else { + clearDirectRetrySlot(i); + } + cleared = true; + } + + return cleared; +} + +void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active) { + continue; + } + + if (_direct_retries[i].queued) { + if (_direct_retries[i].packet == packet) { + // The retry packet itself just finished transmitting; Dispatcher will release it after this hook. + clearDirectRetrySlot(i); + } + continue; + } + + if (_direct_retries[i].trigger_packet != packet) { + continue; + } + + // Allocate the retry packet only after TX-complete so busy repeaters do not reserve pool slots early. + Packet* retry = obtainNewPacket(); + if (retry == NULL) { + clearDirectRetrySlot(i); + continue; + } + + *retry = *packet; + + // Start the echo wait only after the initial direct transmission actually completed. + sendPacket(retry, _direct_retries[i].priority, _direct_retries[i].retry_delay); + if (isDirectRetryQueued(retry)) { + _direct_retries[i].packet = retry; + _direct_retries[i].trigger_packet = NULL; + _direct_retries[i].queued = true; + _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); + } else { + clearDirectRetrySlot(i); + } + } +} + +void Mesh::clearPendingDirectRetryOnSendFail(const Packet* packet) { + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active) { + continue; + } + + if (_direct_retries[i].queued) { + if (_direct_retries[i].packet == packet) { + // The queued retry itself failed; Dispatcher will release it after this hook. + clearDirectRetrySlot(i); + } + continue; + } + + if (_direct_retries[i].trigger_packet == packet) { + clearDirectRetrySlot(i); + } + } +} + +bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len, + uint8_t& progress_marker, bool& expect_path_growth) const { + switch (packet->getPayloadType()) { + case PAYLOAD_TYPE_ACK: + case PAYLOAD_TYPE_PATH: + case PAYLOAD_TYPE_REQ: + case PAYLOAD_TYPE_RESPONSE: + case PAYLOAD_TYPE_TXT_MSG: + case PAYLOAD_TYPE_ANON_REQ: + if (packet->getPathHashCount() <= 1) { + return false; + } + next_hop_hash = packet->path; + next_hop_hash_len = packet->getPathHashSize(); + progress_marker = packet->getPathHashCount(); + expect_path_growth = false; + return true; + + case PAYLOAD_TYPE_MULTIPART: + if (packet->payload_len < 1 || (packet->payload[0] & 0x0F) != PAYLOAD_TYPE_ACK || packet->getPathHashCount() <= 1) { + return false; + } + next_hop_hash = packet->path; + next_hop_hash_len = packet->getPathHashSize(); + progress_marker = packet->getPathHashCount(); + expect_path_growth = false; + return true; + + case PAYLOAD_TYPE_TRACE: { + if (packet->payload_len < 9) { + return false; + } + + uint8_t hash_size = 1 << (packet->payload[8] & 0x03); + uint8_t route_bytes = packet->payload_len - 9; + uint8_t offset = packet->path_len * hash_size; + if (offset + hash_size > route_bytes) { + return false; + } + if (offset + (2 * hash_size) > route_bytes) { + return false; // no downstream repeater means there will be no forward echo to overhear. + } + + next_hop_hash = &packet->payload[9 + offset]; + next_hop_hash_len = hash_size; + progress_marker = packet->path_len; + expect_path_growth = true; + return true; + } + + default: + return false; + } +} + +void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { + const uint8_t* next_hop_hash; + uint8_t next_hop_hash_len; + uint8_t progress_marker; + bool expect_path_growth; + if (!getDirectRetryTarget(packet, next_hop_hash, next_hop_hash_len, progress_marker, expect_path_growth) + || !allowDirectRetry(packet, next_hop_hash, next_hop_hash_len)) { + return; + } + + int slot_idx = -1; + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active) { + slot_idx = i; + break; + } + } + if (slot_idx < 0) { + return; + } + + // Only store retry metadata here; allocate the retry packet after the initial TX really completes. + uint32_t retry_delay = getDirectRetryEchoDelay(packet); + calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key); + _direct_retries[slot_idx].packet = NULL; + _direct_retries[slot_idx].trigger_packet = const_cast(packet); + _direct_retries[slot_idx].retry_at = 0; + _direct_retries[slot_idx].retry_delay = retry_delay; + _direct_retries[slot_idx].priority = priority; + _direct_retries[slot_idx].progress_marker = progress_marker; + _direct_retries[slot_idx].expect_path_growth = expect_path_growth; + _direct_retries[slot_idx].queued = false; + _direct_retries[slot_idx].active = true; +} + Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) { if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL; @@ -634,7 +891,7 @@ void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) { packet->header |= ROUTE_TYPE_FLOOD; packet->path_len = 0; - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us uint8_t pri; if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { @@ -659,7 +916,7 @@ void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_m packet->transport_codes[1] = transport_codes[1]; packet->path_len = 0; - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us uint8_t pri; if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { @@ -692,7 +949,8 @@ void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uin pri = 0; } } - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us + maybeScheduleDirectRetry(packet, pri); sendPacket(packet, pri, delay_millis); } @@ -702,7 +960,7 @@ void Mesh::sendZeroHop(Packet* packet, uint32_t delay_millis) { packet->path_len = 0; // path_len of zero means Zero Hop - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us sendPacket(packet, 0, delay_millis); } @@ -715,9 +973,9 @@ void Mesh::sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay packet->path_len = 0; // path_len of zero means Zero Hop - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us sendPacket(packet, 0, delay_millis); } -} \ No newline at end of file +} diff --git a/src/Mesh.h b/src/Mesh.h index 00f7ed00f4..874336b0f0 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -4,6 +4,10 @@ namespace mesh { +#ifndef MAX_DIRECT_RETRY_SLOTS + #define MAX_DIRECT_RETRY_SLOTS 6 +#endif + class GroupChannel { public: uint8_t hash[PATH_HASH_SIZE]; @@ -16,6 +20,7 @@ class GroupChannel { class MeshTables { public: virtual bool hasSeen(const Packet* packet) = 0; + virtual void markSent(const Packet* packet) = 0; virtual void clear(const Packet* packet) = 0; // remove this packet hash from table }; @@ -24,17 +29,42 @@ class MeshTables { * and provides virtual methods for sub-classes on handling incoming, and also preparing outbound Packets. */ class Mesh : public Dispatcher { + struct DirectRetryEntry { + Packet* packet; + Packet* trigger_packet; + unsigned long retry_at; + uint32_t retry_delay; + uint8_t retry_key[MAX_HASH_SIZE]; + uint8_t priority; + uint8_t progress_marker; + bool expect_path_growth; + bool queued; + bool active; + }; + RTCClock* _rtc; RNG* _rng; MeshTables* _tables; + DirectRetryEntry _direct_retries[MAX_DIRECT_RETRY_SLOTS]; void removeSelfFromPath(Packet* packet); void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis); + void clearDirectRetrySlot(int idx); + bool isDirectRetryQueued(const Packet* packet) const; + void calculateDirectRetryKey(const Packet* packet, uint8_t* dest_key) const; + bool cancelDirectRetryOnEcho(const Packet* packet); + void armDirectRetryOnSendComplete(const Packet* packet); + void clearPendingDirectRetryOnSendFail(const Packet* packet); + bool getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len, + uint8_t& progress_marker, bool& expect_path_growth) const; + void maybeScheduleDirectRetry(const Packet* packet, uint8_t priority); //void routeRecvAcks(Packet* packet, uint32_t delay_millis); DispatcherAction forwardMultipartDirect(Packet* pkt); protected: DispatcherAction onRecvPacket(Packet* pkt) override; + void onSendComplete(Packet* packet) override; + void onSendFail(Packet* packet) override; virtual uint32_t getCADFailRetryDelay() const override; @@ -65,6 +95,17 @@ class Mesh : public Dispatcher { */ virtual uint32_t getDirectRetransmitDelay(const Packet* packet); + /** + * \brief Decide whether a DIRECT packet should get one delayed retry if the next hop echo is not overheard. + * Sub-classes can use neighbour tables or other link-quality data to opt in selectively. + */ + virtual bool allowDirectRetry(const Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const; + + /** + * \returns milliseconds to wait for the next-hop echo before queueing one retry of the DIRECT packet. + */ + virtual uint32_t getDirectRetryEchoDelay(const Packet* packet) const; + /** * \returns number of extra (Direct) ACK transmissions wanted. */ diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 10ab866912..0f954efce6 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -4,6 +4,17 @@ #include "AdvertDataHelpers.h" #include +#ifndef BRIDGE_MAX_BAUD +#define BRIDGE_MAX_BAUD 115200 +#endif + +// These bytes used to be reserved/unused in persisted prefs, so keep a marker before trusting them. +#define DIRECT_RETRY_PREFS_MAGIC_0 0xD4 +#define DIRECT_RETRY_PREFS_MAGIC_1 0x52 +#define DIRECT_RETRY_RECENT_DEFAULT 0 +#define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT 5 +#define DIRECT_RETRY_SNR_MARGIN_DB_MAX 40 + // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -56,7 +67,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84 file.read((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88 file.read((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104 - file.read(pad, 4); // 108 + file.read((uint8_t *)&_prefs->direct_retry_recent_enabled, sizeof(_prefs->direct_retry_recent_enabled)); // 108 + file.read((uint8_t *)&_prefs->direct_retry_snr_margin_db, sizeof(_prefs->direct_retry_snr_margin_db)); // 109 + file.read((uint8_t *)&_prefs->direct_retry_prefs_magic[0], sizeof(_prefs->direct_retry_prefs_magic)); // 110 file.read((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112 file.read((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113 file.read((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 @@ -95,6 +108,15 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, 1, 30); _prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1); _prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 10.0f); + // Old firmware left offset 108..111 undefined, so require the marker before using the new retry prefs. + if (_prefs->direct_retry_prefs_magic[0] != DIRECT_RETRY_PREFS_MAGIC_0 + || _prefs->direct_retry_prefs_magic[1] != DIRECT_RETRY_PREFS_MAGIC_1) { + _prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT; + _prefs->direct_retry_snr_margin_db = DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT; + } else { + _prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1); + _prefs->direct_retry_snr_margin_db = constrain(_prefs->direct_retry_snr_margin_db, 0, DIRECT_RETRY_SNR_MARGIN_DB_MAX); + } // sanitise bad bridge pref values _prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1); @@ -140,7 +162,11 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84 file.write((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88 file.write((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104 - file.write(pad, 4); // 108 + file.write((uint8_t *)&_prefs->direct_retry_recent_enabled, sizeof(_prefs->direct_retry_recent_enabled)); // 108 + file.write((uint8_t *)&_prefs->direct_retry_snr_margin_db, sizeof(_prefs->direct_retry_snr_margin_db)); // 109 + // Persist a marker so later loads can distinguish real values from legacy garbage in this reserved slot. + uint8_t retry_magic[2] = { DIRECT_RETRY_PREFS_MAGIC_0, DIRECT_RETRY_PREFS_MAGIC_1 }; + file.write(retry_magic, sizeof(retry_magic)); // 110 file.write((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112 file.write((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113 file.write((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 @@ -316,6 +342,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); } else if (memcmp(config, "direct.txdelay", 14) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); + } else if (memcmp(config, "direct.retry.heard", 18) == 0) { + sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); + } else if (memcmp(config, "direct.retry.margin", 19) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_snr_margin_db); } else if (memcmp(config, "owner.info", 10) == 0) { *reply++ = '>'; *reply++ = ' '; @@ -535,6 +565,27 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else { strcpy(reply, "Error, cannot be negative"); } + } else if (memcmp(config, "direct.retry.heard ", 19) == 0) { + if (memcmp(&config[19], "on", 2) == 0) { + _prefs->direct_retry_recent_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(&config[19], "off", 3) == 0) { + _prefs->direct_retry_recent_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be on or off"); + } + } else if (memcmp(config, "direct.retry.margin ", 20) == 0) { + int db = atoi(&config[20]); + if (db >= 0 && db <= DIRECT_RETRY_SNR_MARGIN_DB_MAX) { + _prefs->direct_retry_snr_margin_db = (uint8_t)db; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min 0 and max %d", DIRECT_RETRY_SNR_MARGIN_DB_MAX); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 8661d1e6d8..d0f26bfe65 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -27,7 +27,9 @@ struct NodePrefs { // persisted to file float tx_delay_factor; char guest_password[16]; float direct_tx_delay_factor; - uint32_t guard; + uint8_t direct_retry_recent_enabled; + uint8_t direct_retry_snr_margin_db; + uint8_t direct_retry_prefs_magic[2]; uint8_t sf; uint8_t cr; uint8_t allow_read_only; diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 2f8af52af1..217fd5a08c 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -8,13 +8,103 @@ #define MAX_PACKET_HASHES 128 #define MAX_PACKET_ACKS 64 +#define MAX_RECENT_REPEATERS 64 +#define MAX_ROUTE_HASH_BYTES 3 class SimpleMeshTables : public mesh::MeshTables { +public: + struct RecentRepeaterInfo { + // Just enough identity to match a next-hop path prefix plus the SNR that heard it. + uint8_t prefix[MAX_ROUTE_HASH_BYTES]; + uint8_t prefix_len; + int8_t snr_x4; + }; + +private: uint8_t _hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE]; int _next_idx; uint32_t _acks[MAX_PACKET_ACKS]; int _next_ack_idx; uint32_t _direct_dups, _flood_dups; + RecentRepeaterInfo _recent_repeaters[MAX_RECENT_REPEATERS]; + int _next_recent_repeater_idx; + + bool hasSeenAck(uint32_t ack) const { + for (int i = 0; i < MAX_PACKET_ACKS; i++) { + if (ack == _acks[i]) { + return true; + } + } + return false; + } + + void storeAck(uint32_t ack) { + _acks[_next_ack_idx] = ack; + _next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS; + } + + bool hasSeenHash(const uint8_t* hash) const { + const uint8_t* sp = _hashes; + for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { + if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { + return true; + } + } + return false; + } + + void storeHash(const uint8_t* hash) { + memcpy(&_hashes[_next_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE); + _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; + } + + bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { + // Learn repeater prefixes only from packet shapes that expose a trustworthy repeater ID. + if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->payload_len >= PUB_KEY_SIZE) { + memcpy(prefix, packet->payload, MAX_ROUTE_HASH_BYTES); + prefix_len = MAX_ROUTE_HASH_BYTES; + return true; + } + + if (packet->getPayloadType() == PAYLOAD_TYPE_CONTROL + && packet->isRouteDirect() + && packet->getPathHashCount() == 0 + && packet->payload_len >= 6 + MAX_ROUTE_HASH_BYTES + && (packet->payload[0] & 0xF0) == 0x90) { + memcpy(prefix, &packet->payload[6], MAX_ROUTE_HASH_BYTES); + prefix_len = MAX_ROUTE_HASH_BYTES; + return true; + } + + if (packet->isRouteFlood() && packet->getPathHashCount() > 0) { + prefix_len = packet->getPathHashSize(); + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + const uint8_t* last_hop = &packet->path[(packet->getPathHashCount() - 1) * packet->getPathHashSize()]; + memcpy(prefix, last_hop, prefix_len); + return true; + } + + return false; + } + + void recordRecentRepeater(const mesh::Packet* packet) { + uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; + uint8_t prefix_len = 0; + if (!extractRecentRepeater(packet, prefix, prefix_len) || prefix_len == 0) { + return; + } + + // Ring buffer is enough here; retry fallback only needs a recent prefix->SNR observation. + RecentRepeaterInfo& slot = _recent_repeaters[_next_recent_repeater_idx]; + memset(slot.prefix, 0, sizeof(slot.prefix)); + memcpy(slot.prefix, prefix, prefix_len); + slot.prefix_len = prefix_len; + slot.snr_x4 = packet->_snr; + _next_recent_repeater_idx = (_next_recent_repeater_idx + 1) % MAX_RECENT_REPEATERS; + } public: SimpleMeshTables() { @@ -23,6 +113,8 @@ class SimpleMeshTables : public mesh::MeshTables { memset(_acks, 0, sizeof(_acks)); _next_ack_idx = 0; _direct_dups = _flood_dups = 0; + memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); + _next_recent_repeater_idx = 0; } #ifdef ESP32 @@ -31,12 +123,16 @@ class SimpleMeshTables : public mesh::MeshTables { f.read((uint8_t *) &_next_idx, sizeof(_next_idx)); f.read((uint8_t *) &_acks[0], sizeof(_acks)); f.read((uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx)); + f.read((uint8_t *) &_recent_repeaters[0], sizeof(_recent_repeaters)); + f.read((uint8_t *) &_next_recent_repeater_idx, sizeof(_next_recent_repeater_idx)); } void saveTo(File f) { f.write(_hashes, sizeof(_hashes)); f.write((const uint8_t *) &_next_idx, sizeof(_next_idx)); f.write((const uint8_t *) &_acks[0], sizeof(_acks)); f.write((const uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx)); + f.write((const uint8_t *) &_recent_repeaters[0], sizeof(_recent_repeaters)); + f.write((const uint8_t *) &_next_recent_repeater_idx, sizeof(_next_recent_repeater_idx)); } #endif @@ -44,42 +140,55 @@ class SimpleMeshTables : public mesh::MeshTables { if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { uint32_t ack; memcpy(&ack, packet->payload, 4); - for (int i = 0; i < MAX_PACKET_ACKS; i++) { - if (ack == _acks[i]) { - if (packet->isRouteDirect()) { - _direct_dups++; // keep some stats - } else { - _flood_dups++; - } - return true; + + if (hasSeenAck(ack)) { + if (packet->isRouteDirect()) { + _direct_dups++; // keep some stats + } else { + _flood_dups++; } + return true; } - - _acks[_next_ack_idx] = ack; - _next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS; // cyclic table + + storeAck(ack); return false; } uint8_t hash[MAX_HASH_SIZE]; packet->calculatePacketHash(hash); - const uint8_t* sp = _hashes; - for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { - if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { - if (packet->isRouteDirect()) { - _direct_dups++; // keep some stats - } else { - _flood_dups++; - } - return true; + if (hasSeenHash(hash)) { + if (packet->isRouteDirect()) { + _direct_dups++; // keep some stats + } else { + _flood_dups++; } + return true; } - memcpy(&_hashes[_next_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE); - _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; // cyclic table + storeHash(hash); + recordRecentRepeater(packet); return false; } + void markSent(const mesh::Packet* packet) override { + // Outbound packets must be marked as already-sent without teaching the recent-heard cache about ourselves. + if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { + uint32_t ack; + memcpy(&ack, packet->payload, 4); + if (!hasSeenAck(ack)) { + storeAck(ack); + } + return; + } + + uint8_t hash[MAX_HASH_SIZE]; + packet->calculatePacketHash(hash); + if (!hasSeenHash(hash)) { + storeHash(hash); + } + } + void clear(const mesh::Packet* packet) override { if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { uint32_t ack; @@ -107,5 +216,24 @@ class SimpleMeshTables : public mesh::MeshTables { uint32_t getNumDirectDups() const { return _direct_dups; } uint32_t getNumFloodDups() const { return _flood_dups; } + const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t* hash, uint8_t hash_len) const { + if (hash == NULL || hash_len == 0) { + return NULL; + } + + // Search newest-to-oldest so the retry gate prefers the freshest SNR sample for a prefix. + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; + const RecentRepeaterInfo* info = &_recent_repeaters[idx]; + if (info->prefix_len < hash_len || info->prefix_len == 0) { + continue; + } + if (memcmp(info->prefix, hash, hash_len) == 0) { + return info; + } + } + return NULL; + } + void resetStats() { _direct_dups = _flood_dups = 0; } }; From dbc4dda0ca67a84f2bfccbd950e0608527f8e0ab Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 23 Apr 2026 16:25:51 -0700 Subject: [PATCH 64/94] Retry 3 times with a 200ms,300ms,400ms backoff. --- docs/cli_commands.md | 13 +++++ examples/simple_repeater/MyMesh.cpp | 85 ++++++++++++++++++++++++++++ examples/simple_repeater/MyMesh.h | 2 + src/Dispatcher.h | 2 + src/Mesh.cpp | 55 +++++++++++++++++- src/Mesh.h | 6 ++ src/helpers/SimpleMeshTables.h | 86 +++++++++++++++++++++++++---- 7 files changed, 236 insertions(+), 13 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index c6a7fac01b..9fa175a258 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -98,6 +98,19 @@ --- +### Get or set recent repeater fallback prefix/SNR +**Usage:** +- `recent.repeater` +- `recent.repeater ` + +**Parameters:** +- `prefix_hex`: 1-3 bytes of next-hop prefix (hex) +- `snr_db`: SNR in dB (supports decimals; stored at x4 precision) + +**Note:** `set` is rejected when the prefix already exists in neighbors. + +--- + ## Statistics ### Clear Stats diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index f268679c64..9d9e7d03b6 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -77,6 +77,15 @@ const NeighbourInfo* MyMesh::findNeighbourByHash(const uint8_t* hash, uint8_t ha return NULL; } +bool MyMesh::allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefix_len, void* ctx) { + if (ctx == NULL || prefix == NULL || prefix_len == 0) { + return true; + } + + const MyMesh* self = (const MyMesh*) ctx; + return self->findNeighbourByHash(prefix, prefix_len) == NULL; +} + void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { #if MAX_NEIGHBOURS // check if neighbours enabled // find existing neighbour, else use least recently updated @@ -510,6 +519,34 @@ void MyMesh::logTxFail(mesh::Packet *pkt, int len) { } } +void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis) { + if (packet == NULL) { + return; + } + + MESH_DEBUG_PRINTLN("%s direct retry %s (type=%d, route=%s, payload_len=%d, delay=%lu)", + getLogDateTime(), + event, + (uint32_t)packet->getPayloadType(), + packet->isRouteDirect() ? "D" : "F", + (uint32_t)packet->payload_len, + (unsigned long)delay_millis); + + if (_logging) { + File f = openAppend(PACKET_LOG_FILE); + if (f) { + f.print(getLogDateTime()); + f.printf(": DIRECT RETRY %s (type=%d, route=%s, payload_len=%d, delay=%lu)\n", + event, + (uint32_t)packet->getPayloadType(), + packet->isRouteDirect() ? "D" : "F", + (uint32_t)packet->payload_len, + (unsigned long)delay_millis); + f.close(); + } + } +} + int MyMesh::calcRxDelay(float score, uint32_t air_time) const { if (_prefs.rx_delay_base <= 0.0f) return 0; return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); @@ -880,6 +917,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc active_bw = _prefs.bw; active_sf = _prefs.sf; active_cr = _prefs.cr; + + ((SimpleMeshTables *)getTables())->setRecentRepeaterAllowFilter(&MyMesh::allowRecentRepeaterPrefixStore, this); } void MyMesh::begin(FILESYSTEM *fs) { @@ -901,6 +940,7 @@ void MyMesh::begin(FILESYSTEM *fs) { active_bw = _prefs.bw; active_sf = _prefs.sf; active_cr = _prefs.cr; + ((SimpleMeshTables *)getTables())->setRecentRepeaterMinSNRX4(getDirectRetryMinSNRX4()); radio_set_tx_power(_prefs.tx_power_dbm); updateAdvertTimer(); @@ -1250,6 +1290,48 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else { strcpy(reply, "Err - ??"); } + } else if (memcmp(command, "recent.repeater", 15) == 0) { + const char* sub = command + 15; + while (*sub == ' ') sub++; + auto* tables = (SimpleMeshTables*)getTables(); + if (*sub == 0) { + const auto* info = tables->getLatestRecentRepeater(); + if (info == NULL) { + strcpy(reply, "> none"); + } else { + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; + mesh::Utils::toHex(hex, info->prefix, info->prefix_len); + sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + } + } else { + char* params = (char*) sub; + char* arg_snr = strchr(params, ' '); + if (arg_snr == NULL) { + strcpy(reply, "Err - usage: recent.repeater "); + } else { + *arg_snr++ = 0; + while (*arg_snr == ' ') arg_snr++; + if (*arg_snr == 0) { + strcpy(reply, "Err - usage: recent.repeater "); + } else { + int hex_len = strlen(params); + int prefix_len = hex_len / 2; + uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; + if ((hex_len % 2) != 0 || prefix_len <= 0 || prefix_len > MAX_ROUTE_HASH_BYTES || !mesh::Utils::fromHex(prefix, prefix_len, params)) { + strcpy(reply, "Err - prefix must be 1-3 bytes hex"); + } else { + float snr_db = strtof(arg_snr, nullptr); + int snr_x4 = (int)(snr_db * 4.0f + (snr_db >= 0.0f ? 0.5f : -0.5f)); + snr_x4 = constrain(snr_x4, -128, 127); + if (tables->setRecentRepeater(prefix, (uint8_t)prefix_len, (int8_t)snr_x4)) { + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - prefix is already in neighbors"); + } + } + } + } + } } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands } @@ -1293,6 +1375,9 @@ void MyMesh::loop() { MESH_DEBUG_PRINTLN("Radio params restored"); } + // Keep recent-prefix learning aligned with the live retry SNR gate. + ((SimpleMeshTables *)getTables())->setRecentRepeaterMinSNRX4(getDirectRetryMinSNRX4()); + // is pending dirty contacts write needed? if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { acl.save(_fs); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 315288296a..4b5d7b1f35 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -119,6 +119,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { #endif const NeighbourInfo* findNeighbourByHash(const uint8_t* hash, uint8_t hash_len) const; + static bool allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefix_len, void* ctx); int8_t getDirectRetryMinSNRX4() const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); @@ -148,6 +149,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override; uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override; + void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis) override; int getInterferenceThreshold() const override { return _prefs.interference_threshold; diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 29460061f4..970e19757e 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -161,6 +161,8 @@ class Dispatcher { virtual uint32_t getCADFailMaxDuration() const; virtual int getInterferenceThreshold() const { return 0; } // disabled by default virtual int getAGCResetInterval() const { return 0; } // disabled by default + virtual unsigned long getDutyCycleWindowMs() const { return 3600000; } + const Packet* getOutboundInFlight() const { return outbound; } public: void begin(); diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 082b6bb444..b4710b8c43 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -3,12 +3,16 @@ namespace mesh { +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS = 3; +static const uint32_t DIRECT_RETRY_BACKOFF_MS[DIRECT_RETRY_MAX_ATTEMPTS] = { 200, 300, 400 }; + void Mesh::begin() { for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { _direct_retries[i].packet = NULL; _direct_retries[i].trigger_packet = NULL; _direct_retries[i].retry_at = 0; _direct_retries[i].retry_delay = 0; + _direct_retries[i].retry_attempts_sent = 0; _direct_retries[i].priority = 0; _direct_retries[i].progress_marker = 0; _direct_retries[i].expect_path_growth = false; @@ -27,6 +31,9 @@ void Mesh::loop() { } if (!isDirectRetryQueued(_direct_retries[i].packet)) { + if (_direct_retries[i].packet == getOutboundInFlight()) { + continue; // currently transmitting; keep slot until onSendComplete/onSendFail emits event + } clearDirectRetrySlot(i); } } @@ -443,6 +450,7 @@ void Mesh::clearDirectRetrySlot(int idx) { _direct_retries[idx].trigger_packet = NULL; _direct_retries[idx].retry_at = 0; _direct_retries[idx].retry_delay = 0; + _direct_retries[idx].retry_attempts_sent = 0; _direct_retries[idx].priority = 0; _direct_retries[idx].progress_marker = 0; _direct_retries[idx].expect_path_growth = false; @@ -491,8 +499,12 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { break; } } + onDirectRetryEvent("canceled_echo", _direct_retries[i].packet, 0); + onDirectRetryEvent("good", _direct_retries[i].packet, 0); clearDirectRetrySlot(i); } else { + onDirectRetryEvent("canceled_echo", _direct_retries[i].trigger_packet, 0); + onDirectRetryEvent("good", _direct_retries[i].trigger_packet, 0); clearDirectRetrySlot(i); } cleared = true; @@ -510,7 +522,35 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { if (_direct_retries[i].queued) { if (_direct_retries[i].packet == packet) { // The retry packet itself just finished transmitting; Dispatcher will release it after this hook. - clearDirectRetrySlot(i); + onDirectRetryEvent("resent", packet, 0); + _direct_retries[i].retry_attempts_sent++; + if (_direct_retries[i].retry_attempts_sent >= DIRECT_RETRY_MAX_ATTEMPTS) { + onDirectRetryEvent("failure", packet, 0); + clearDirectRetrySlot(i); + continue; + } + + Packet* retry = obtainNewPacket(); + if (retry == NULL) { + onDirectRetryEvent("dropped_no_packet", packet, 0); + onDirectRetryEvent("failure", packet, 0); + clearDirectRetrySlot(i); + continue; + } + + *retry = *packet; + uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[_direct_retries[i].retry_attempts_sent]; + sendPacket(retry, _direct_retries[i].priority, retry_delay); + if (isDirectRetryQueued(retry)) { + _direct_retries[i].packet = retry; + _direct_retries[i].retry_delay = retry_delay; + _direct_retries[i].retry_at = futureMillis(retry_delay); + onDirectRetryEvent("queued", retry, retry_delay); + } else { + onDirectRetryEvent("dropped_queue_full", retry, retry_delay); + onDirectRetryEvent("failure", retry, 0); + clearDirectRetrySlot(i); + } } continue; } @@ -522,6 +562,8 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { // Allocate the retry packet only after TX-complete so busy repeaters do not reserve pool slots early. Packet* retry = obtainNewPacket(); if (retry == NULL) { + onDirectRetryEvent("dropped_no_packet", packet, _direct_retries[i].retry_delay); + onDirectRetryEvent("failure", packet, 0); clearDirectRetrySlot(i); continue; } @@ -535,7 +577,10 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { _direct_retries[i].trigger_packet = NULL; _direct_retries[i].queued = true; _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); + onDirectRetryEvent("queued", retry, _direct_retries[i].retry_delay); } else { + onDirectRetryEvent("dropped_queue_full", retry, _direct_retries[i].retry_delay); + onDirectRetryEvent("failure", retry, 0); clearDirectRetrySlot(i); } } @@ -550,12 +595,16 @@ void Mesh::clearPendingDirectRetryOnSendFail(const Packet* packet) { if (_direct_retries[i].queued) { if (_direct_retries[i].packet == packet) { // The queued retry itself failed; Dispatcher will release it after this hook. + onDirectRetryEvent("dropped_send_fail", packet, 0); + onDirectRetryEvent("failure", packet, 0); clearDirectRetrySlot(i); } continue; } if (_direct_retries[i].trigger_packet == packet) { + onDirectRetryEvent("dropped_send_fail", packet, 0); + onDirectRetryEvent("failure", packet, 0); clearDirectRetrySlot(i); } } @@ -638,17 +687,19 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { } // Only store retry metadata here; allocate the retry packet after the initial TX really completes. - uint32_t retry_delay = getDirectRetryEchoDelay(packet); + uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[0]; calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key); _direct_retries[slot_idx].packet = NULL; _direct_retries[slot_idx].trigger_packet = const_cast(packet); _direct_retries[slot_idx].retry_at = 0; _direct_retries[slot_idx].retry_delay = retry_delay; + _direct_retries[slot_idx].retry_attempts_sent = 0; _direct_retries[slot_idx].priority = priority; _direct_retries[slot_idx].progress_marker = progress_marker; _direct_retries[slot_idx].expect_path_growth = expect_path_growth; _direct_retries[slot_idx].queued = false; _direct_retries[slot_idx].active = true; + onDirectRetryEvent("armed", packet, retry_delay); } Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) { diff --git a/src/Mesh.h b/src/Mesh.h index 874336b0f0..89b6bed448 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -34,6 +34,7 @@ class Mesh : public Dispatcher { Packet* trigger_packet; unsigned long retry_at; uint32_t retry_delay; + uint8_t retry_attempts_sent; uint8_t retry_key[MAX_HASH_SIZE]; uint8_t priority; uint8_t progress_marker; @@ -111,6 +112,11 @@ class Mesh : public Dispatcher { */ virtual uint8_t getExtraAckTransmitCount() const; + /** + * \brief Optional hook for logging direct-retry lifecycle events. + */ + virtual void onDirectRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis) { } + /** * \brief Perform search of local DB of peers/contacts. * \returns Number of peers with matching hash diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 217fd5a08c..f5da272b1b 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -13,6 +13,8 @@ class SimpleMeshTables : public mesh::MeshTables { public: + typedef bool (*RecentRepeaterAllowFn)(const uint8_t* prefix, uint8_t prefix_len, void* ctx); + struct RecentRepeaterInfo { // Just enough identity to match a next-hop path prefix plus the SNR that heard it. uint8_t prefix[MAX_ROUTE_HASH_BYTES]; @@ -28,6 +30,9 @@ class SimpleMeshTables : public mesh::MeshTables { uint32_t _direct_dups, _flood_dups; RecentRepeaterInfo _recent_repeaters[MAX_RECENT_REPEATERS]; int _next_recent_repeater_idx; + int8_t _recent_repeater_min_snr_x4; + RecentRepeaterAllowFn _recent_repeater_allow_fn; + void* _recent_repeater_allow_ctx; bool hasSeenAck(uint32_t ack) const { for (int i = 0; i < MAX_PACKET_ACKS; i++) { @@ -58,6 +63,11 @@ class SimpleMeshTables : public mesh::MeshTables { _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; } + bool prefixesOverlap(const uint8_t* a, uint8_t a_len, const uint8_t* b, uint8_t b_len) const { + uint8_t n = a_len < b_len ? a_len : b_len; + return n > 0 && memcmp(a, b, n) == 0; + } + bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { // Learn repeater prefixes only from packet shapes that expose a trustworthy repeater ID. if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->payload_len >= PUB_KEY_SIZE) { @@ -96,14 +106,10 @@ class SimpleMeshTables : public mesh::MeshTables { if (!extractRecentRepeater(packet, prefix, prefix_len) || prefix_len == 0) { return; } - - // Ring buffer is enough here; retry fallback only needs a recent prefix->SNR observation. - RecentRepeaterInfo& slot = _recent_repeaters[_next_recent_repeater_idx]; - memset(slot.prefix, 0, sizeof(slot.prefix)); - memcpy(slot.prefix, prefix, prefix_len); - slot.prefix_len = prefix_len; - slot.snr_x4 = packet->_snr; - _next_recent_repeater_idx = (_next_recent_repeater_idx + 1) % MAX_RECENT_REPEATERS; + if (packet->_snr < _recent_repeater_min_snr_x4) { + return; + } + setRecentRepeater(prefix, prefix_len, packet->_snr); } public: @@ -115,6 +121,9 @@ class SimpleMeshTables : public mesh::MeshTables { _direct_dups = _flood_dups = 0; memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); _next_recent_repeater_idx = 0; + _recent_repeater_min_snr_x4 = -128; + _recent_repeater_allow_fn = NULL; + _recent_repeater_allow_ctx = NULL; } #ifdef ESP32 @@ -216,19 +225,74 @@ class SimpleMeshTables : public mesh::MeshTables { uint32_t getNumDirectDups() const { return _direct_dups; } uint32_t getNumFloodDups() const { return _flood_dups; } + void setRecentRepeaterMinSNRX4(int8_t min_snr_x4) { + _recent_repeater_min_snr_x4 = min_snr_x4; + } + void setRecentRepeaterAllowFilter(RecentRepeaterAllowFn fn, void* ctx) { + _recent_repeater_allow_fn = fn; + _recent_repeater_allow_ctx = ctx; + } + bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) { + if (prefix == NULL || prefix_len == 0) { + return false; + } + + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + if (_recent_repeater_allow_fn != NULL && !_recent_repeater_allow_fn(prefix, prefix_len, _recent_repeater_allow_ctx)) { + return false; + } + + // Keep one slot for overlapping prefixes so 1/2/3-byte paths share the same entry. + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (prefix_len > existing.prefix_len) { + memset(existing.prefix, 0, sizeof(existing.prefix)); + memcpy(existing.prefix, prefix, prefix_len); + existing.prefix_len = prefix_len; + } + existing.snr_x4 = snr_x4; + return true; + } + + // Ring buffer is enough here; retry fallback only needs a recent prefix->SNR observation. + RecentRepeaterInfo& slot = _recent_repeaters[_next_recent_repeater_idx]; + memset(slot.prefix, 0, sizeof(slot.prefix)); + memcpy(slot.prefix, prefix, prefix_len); + slot.prefix_len = prefix_len; + slot.snr_x4 = snr_x4; + _next_recent_repeater_idx = (_next_recent_repeater_idx + 1) % MAX_RECENT_REPEATERS; + return true; + } + const RecentRepeaterInfo* getLatestRecentRepeater() const { + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; + const RecentRepeaterInfo* info = &_recent_repeaters[idx]; + if (info->prefix_len > 0) { + return info; + } + } + return NULL; + } + const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t* hash, uint8_t hash_len) const { if (hash == NULL || hash_len == 0) { return NULL; } - // Search newest-to-oldest so the retry gate prefers the freshest SNR sample for a prefix. + // Search newest-to-oldest and allow 1/2/3-byte prefixes to overlap-match. for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; const RecentRepeaterInfo* info = &_recent_repeaters[idx]; - if (info->prefix_len < hash_len || info->prefix_len == 0) { + if (info->prefix_len == 0) { continue; } - if (memcmp(info->prefix, hash, hash_len) == 0) { + if (prefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) { return info; } } From b125d0a0d1e9d4cef975ee091f193bf32a7ee2ab Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 24 Apr 2026 00:08:10 -0700 Subject: [PATCH 65/94] Max retries is now a var that can be set between 1 to 15 --- docs/cli_commands.md | 39 +++++++- examples/simple_repeater/MyMesh.cpp | 134 ++++++++++++++++++++++++---- examples/simple_repeater/MyMesh.h | 1 + src/Mesh.cpp | 30 +++++-- src/Mesh.h | 10 +++ src/helpers/CommonCLI.cpp | 55 ++++++++++-- src/helpers/CommonCLI.h | 3 + src/helpers/SimpleMeshTables.h | 45 ++++++++++ 8 files changed, 286 insertions(+), 31 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 9fa175a258..bbd2869340 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -100,14 +100,21 @@ ### Get or set recent repeater fallback prefix/SNR **Usage:** -- `recent.repeater` -- `recent.repeater ` +- `get recent.repeater` +- `get recent.repeater all` +- `get recent.repeater first ` +- `get recent.repeater last ` +- `set recent.repeater ` **Parameters:** - `prefix_hex`: 1-3 bytes of next-hop prefix (hex) - `snr_db`: SNR in dB (supports decimals; stored at x4 precision) +- `count`: number of entries to print -**Note:** `set` is rejected when the prefix already exists in neighbors. +**Notes:** +- `set` is rejected when the prefix already exists in neighbors. +- `all` prints oldest to newest; `first` prints the oldest N; `last` prints the newest N. +- Remote CLI replies include rows too, but may truncate when the packet payload limit is reached. --- @@ -446,6 +453,32 @@ --- +#### View or change the number of direct retry attempts +**Usage:** +- `get direct.retry.count` +- `set direct.retry.count ` + +**Parameters:** +- `value`: Retry attempts after initial TX (`1`-`15`) + +**Default:** `3` + +--- + +#### View or change the base direct retry wait (milliseconds) +**Usage:** +- `get direct.retry.base` +- `set direct.retry.base ` + +**Parameters:** +- `value`: Base wait in milliseconds (`10`-`5000`) + +**Default:** `200` + +**Note:** The actual first retry wait is `base + computed_echo_wait_from_live_phy`. + +--- + #### [Experimental] View or change the processing delay for received traffic **Usage:** - `get rxdelay` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 9d9e7d03b6..3e8985e850 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -587,16 +587,21 @@ bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_ho return recent != NULL && recent->snr_x4 >= min_snr_x4; } uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { + uint32_t base_wait_millis = constrain((uint32_t)_prefs.direct_retry_base_ms, (uint32_t)10, (uint32_t)5000); // Approximate LoRa line rate in kilobits/sec from the live radio params the repeater is using now. float kbps = (((float) active_sf) * active_bw * ((float) active_cr)) / ((float) (1UL << active_sf)); if (kbps <= 0.0f) { - return HALO_DIRECT_RETRY_DELAY_MIN; + return base_wait_millis; } // Wait roughly long enough for our transmission, the next hop's receive/forward window, and its echo back. uint32_t bits = ((uint32_t) packet->getRawLength()) * 8; uint32_t scaled_wait_millis = (uint32_t) ((((float) bits) * 4.0f) / kbps); - return max((uint32_t) HALO_DIRECT_RETRY_DELAY_MIN, scaled_wait_millis); + return base_wait_millis + scaled_wait_millis; +} +uint8_t MyMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const { + (void)packet; + return constrain(_prefs.direct_retry_attempts, (uint8_t)1, (uint8_t)15); } bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { @@ -885,6 +890,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.direct_tx_delay_factor = 0.3f; // was 0.2 _prefs.direct_retry_recent_enabled = 0; _prefs.direct_retry_snr_margin_db = 5; + _prefs.direct_retry_attempts = 3; + _prefs.direct_retry_base_ms = 200; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; _prefs.node_lon = ADVERT_LON; @@ -1290,29 +1297,36 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else { strcpy(reply, "Err - ??"); } - } else if (memcmp(command, "recent.repeater", 15) == 0) { - const char* sub = command + 15; + } else if (strncmp(command, "get recent.repeater", 19) == 0 + || strncmp(command, "set recent.repeater", 19) == 0 + || strncmp(command, "recent.repeater", 15) == 0) { + bool is_get = false; + bool is_set = false; + const char* sub = command; + + if (strncmp(command, "get recent.repeater", 19) == 0) { + is_get = true; + sub = command + 19; + } else if (strncmp(command, "set recent.repeater", 19) == 0) { + is_set = true; + sub = command + 19; + } else { + sub = command + 15; // legacy command format + } while (*sub == ' ') sub++; + auto* tables = (SimpleMeshTables*)getTables(); - if (*sub == 0) { - const auto* info = tables->getLatestRecentRepeater(); - if (info == NULL) { - strcpy(reply, "> none"); - } else { - char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; - mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); - } - } else { + + if (is_set || (!is_get && *sub != 0 && strcmp(sub, "all") != 0 && strncmp(sub, "first ", 6) != 0 && strncmp(sub, "last ", 5) != 0)) { char* params = (char*) sub; char* arg_snr = strchr(params, ' '); if (arg_snr == NULL) { - strcpy(reply, "Err - usage: recent.repeater "); + strcpy(reply, "Err - usage: set recent.repeater "); } else { *arg_snr++ = 0; while (*arg_snr == ' ') arg_snr++; if (*arg_snr == 0) { - strcpy(reply, "Err - usage: recent.repeater "); + strcpy(reply, "Err - usage: set recent.repeater "); } else { int hex_len = strlen(params); int prefix_len = hex_len / 2; @@ -1331,6 +1345,94 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } } + } else if (*sub == 0) { + const auto* info = tables->getLatestRecentRepeater(); + if (info == NULL) { + strcpy(reply, "> none"); + } else { + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; + mesh::Utils::toHex(hex, info->prefix, info->prefix_len); + sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + } + } else if (strcmp(sub, "all") == 0 || strncmp(sub, "first ", 6) == 0 || strncmp(sub, "last ", 5) == 0) { + int total = tables->getRecentRepeaterCount(); + if (total <= 0) { + strcpy(reply, "> none"); + } else { + bool newest_first = false; + int limit = total; + const char* mode = "all"; + if (strncmp(sub, "first ", 6) == 0 || strncmp(sub, "last ", 5) == 0) { + const char* nstr = sub + (sub[0] == 'f' ? 6 : 5); + while (*nstr == ' ') nstr++; + if (*nstr == 0) { + strcpy(reply, "Err - usage: get recent.repeater first|last "); + return; + } + char* end_ptr = NULL; + long parsed = strtol(nstr, &end_ptr, 10); + while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; + if (end_ptr == NULL || *end_ptr != 0 || parsed <= 0) { + strcpy(reply, "Err - count must be > 0"); + return; + } + limit = (int)parsed; + if (sub[0] == 'l') { + newest_first = true; + mode = "last"; + } else { + mode = "first"; + } + } + if (limit > total) { + limit = total; + } + + if (sender_timestamp == 0) { + Serial.printf("Recent repeater table (%s %d/%d):\n", mode, limit, total); + for (int i = 0; i < limit; i++) { + const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(i) : tables->getRecentRepeaterOldestByIdx(i); + if (info == NULL) { + continue; + } + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; + mesh::Utils::toHex(hex, info->prefix, info->prefix_len); + Serial.printf("%02d: %s,%s\n", i + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + } + sprintf(reply, "> showing %d/%d (%s)", limit, total, mode); + } else { + // Remote CLI replies are packet-bound, so include as many rows as fit. + int written = snprintf(reply, 160, "> showing %d/%d (%s)", limit, total, mode); + bool truncated = false; + if (written < 0) { + reply[0] = 0; + written = 0; + } + for (int i = 0; i < limit; i++) { + const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(i) : tables->getRecentRepeaterOldestByIdx(i); + if (info == NULL) { + continue; + } + if (written >= 154) { + truncated = true; + break; + } + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; + mesh::Utils::toHex(hex, info->prefix, info->prefix_len); + int n = snprintf(reply + written, 160 - written, "\n%02d:%s,%s", i + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + if (n < 0 || n >= (160 - written)) { + truncated = true; + break; + } + written += n; + } + if (truncated && written < 156) { + snprintf(reply + written, 160 - written, "\n..."); + } + } + } + } else { + strcpy(reply, "Err - usage: get recent.repeater [all|first |last ]"); } } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 4b5d7b1f35..3ecbfc81e0 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -149,6 +149,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override; uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override; + uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override; void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis) override; int getInterferenceThreshold() const override { diff --git a/src/Mesh.cpp b/src/Mesh.cpp index b4710b8c43..be2c81f1a0 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -3,8 +3,8 @@ namespace mesh { -static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS = 3; -static const uint32_t DIRECT_RETRY_BACKOFF_MS[DIRECT_RETRY_MAX_ATTEMPTS] = { 200, 300, 400 }; +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 3; +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; void Mesh::begin() { for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { @@ -57,6 +57,14 @@ uint32_t Mesh::getDirectRetryEchoDelay(const Packet* packet) const { // Keep the base fallback aligned with the repeater's minimum retry wait. return 200; } +uint8_t Mesh::getDirectRetryMaxAttempts(const Packet* packet) const { + return DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT; +} +uint32_t Mesh::getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) const { + uint32_t base = getDirectRetryEchoDelay(packet); + // Keep the historical linear spacing while allowing the base wait to vary by platform/profile. + return base + ((uint32_t)attempt_idx * 100UL); +} uint8_t Mesh::getExtraAckTransmitCount() const { return 0; } @@ -524,7 +532,13 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { // The retry packet itself just finished transmitting; Dispatcher will release it after this hook. onDirectRetryEvent("resent", packet, 0); _direct_retries[i].retry_attempts_sent++; - if (_direct_retries[i].retry_attempts_sent >= DIRECT_RETRY_MAX_ATTEMPTS) { + uint8_t max_attempts = getDirectRetryMaxAttempts(packet); + if (max_attempts < 1) { + max_attempts = 1; + } else if (max_attempts > DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX) { + max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX; + } + if (_direct_retries[i].retry_attempts_sent >= max_attempts) { onDirectRetryEvent("failure", packet, 0); clearDirectRetrySlot(i); continue; @@ -539,7 +553,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { } *retry = *packet; - uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[_direct_retries[i].retry_attempts_sent]; + uint32_t retry_delay = getDirectRetryAttemptDelay(packet, _direct_retries[i].retry_attempts_sent); sendPacket(retry, _direct_retries[i].priority, retry_delay); if (isDirectRetryQueued(retry)) { _direct_retries[i].packet = retry; @@ -619,7 +633,9 @@ bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_h case PAYLOAD_TYPE_RESPONSE: case PAYLOAD_TYPE_TXT_MSG: case PAYLOAD_TYPE_ANON_REQ: - if (packet->getPathHashCount() <= 1) { + // Allow retries even when only one downstream hop remains so fixed direct paths + // (e.g. remote admin/login over 2-hop chains) use the same retry policy. + if (packet->getPathHashCount() == 0) { return false; } next_hop_hash = packet->path; @@ -629,7 +645,7 @@ bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_h return true; case PAYLOAD_TYPE_MULTIPART: - if (packet->payload_len < 1 || (packet->payload[0] & 0x0F) != PAYLOAD_TYPE_ACK || packet->getPathHashCount() <= 1) { + if (packet->payload_len < 1 || (packet->payload[0] & 0x0F) != PAYLOAD_TYPE_ACK || packet->getPathHashCount() == 0) { return false; } next_hop_hash = packet->path; @@ -687,7 +703,7 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { } // Only store retry metadata here; allocate the retry packet after the initial TX really completes. - uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[0]; + uint32_t retry_delay = getDirectRetryAttemptDelay(packet, 0); calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key); _direct_retries[slot_idx].packet = NULL; _direct_retries[slot_idx].trigger_packet = const_cast(packet); diff --git a/src/Mesh.h b/src/Mesh.h index 89b6bed448..dd58d754aa 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -107,6 +107,16 @@ class Mesh : public Dispatcher { */ virtual uint32_t getDirectRetryEchoDelay(const Packet* packet) const; + /** + * \returns maximum number of retry transmissions after the initial direct TX. + */ + virtual uint8_t getDirectRetryMaxAttempts(const Packet* packet) const; + + /** + * \returns delay before a specific retry attempt, where attempt_idx=0 is the first retry. + */ + virtual uint32_t getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) const; + /** * \returns number of extra (Direct) ACK transmissions wanted. */ diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 0f954efce6..bae389ba55 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -14,6 +14,14 @@ #define DIRECT_RETRY_RECENT_DEFAULT 0 #define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT 5 #define DIRECT_RETRY_SNR_MARGIN_DB_MAX 40 +#define DIRECT_RETRY_TIMING_MAGIC_0 0xD5 +#define DIRECT_RETRY_TIMING_MAGIC_1 0x54 +#define DIRECT_RETRY_COUNT_DEFAULT 3 +#define DIRECT_RETRY_COUNT_MIN 1 +#define DIRECT_RETRY_COUNT_MAX 15 +#define DIRECT_RETRY_BASE_MS_DEFAULT 200 +#define DIRECT_RETRY_BASE_MS_MIN 10 +#define DIRECT_RETRY_BASE_MS_MAX 5000 // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { @@ -92,9 +100,12 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 file.read((uint8_t *)&_prefs->advert_loc_policy, sizeof (_prefs->advert_loc_policy)); // 161 file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 - file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 - file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 - // 290 + file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 + file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 + file.read((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 290 + file.read((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 291 + file.read((uint8_t *)&_prefs->direct_retry_timing_magic[0], sizeof(_prefs->direct_retry_timing_magic)); // 293 + // next: 295 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -117,6 +128,14 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1); _prefs->direct_retry_snr_margin_db = constrain(_prefs->direct_retry_snr_margin_db, 0, DIRECT_RETRY_SNR_MARGIN_DB_MAX); } + if (_prefs->direct_retry_timing_magic[0] != DIRECT_RETRY_TIMING_MAGIC_0 + || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1) { + _prefs->direct_retry_attempts = DIRECT_RETRY_COUNT_DEFAULT; + _prefs->direct_retry_base_ms = DIRECT_RETRY_BASE_MS_DEFAULT; + } else { + _prefs->direct_retry_attempts = constrain(_prefs->direct_retry_attempts, DIRECT_RETRY_COUNT_MIN, DIRECT_RETRY_COUNT_MAX); + _prefs->direct_retry_base_ms = constrain(_prefs->direct_retry_base_ms, DIRECT_RETRY_BASE_MS_MIN, DIRECT_RETRY_BASE_MS_MAX); + } // sanitise bad bridge pref values _prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1); @@ -190,8 +209,12 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161 file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 - file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 - // 290 + file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 + file.write((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 290 + file.write((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 291 + uint8_t retry_timing_magic[2] = { DIRECT_RETRY_TIMING_MAGIC_0, DIRECT_RETRY_TIMING_MAGIC_1 }; + file.write(retry_timing_magic, sizeof(retry_timing_magic)); // 293 + // next: 295 file.close(); } @@ -346,6 +369,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); } else if (memcmp(config, "direct.retry.margin", 19) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_snr_margin_db); + } else if (memcmp(config, "direct.retry.count", 18) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts); + } else if (memcmp(config, "direct.retry.base", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_base_ms); } else if (memcmp(config, "owner.info", 10) == 0) { *reply++ = '>'; *reply++ = ' '; @@ -586,6 +613,24 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else { sprintf(reply, "Error, min 0 and max %d", DIRECT_RETRY_SNR_MARGIN_DB_MAX); } + } else if (memcmp(config, "direct.retry.count ", 19) == 0) { + int count = atoi(&config[19]); + if (count >= DIRECT_RETRY_COUNT_MIN && count <= DIRECT_RETRY_COUNT_MAX) { + _prefs->direct_retry_attempts = (uint8_t)count; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_COUNT_MIN, DIRECT_RETRY_COUNT_MAX); + } + } else if (memcmp(config, "direct.retry.base ", 18) == 0) { + int delay_ms = atoi(&config[18]); + if (delay_ms >= DIRECT_RETRY_BASE_MS_MIN && delay_ms <= DIRECT_RETRY_BASE_MS_MAX) { + _prefs->direct_retry_base_ms = (uint16_t)delay_ms; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_BASE_MS_MIN, DIRECT_RETRY_BASE_MS_MAX); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index d0f26bfe65..ab8e058a51 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -54,6 +54,9 @@ struct NodePrefs { // persisted to file uint32_t discovery_mod_timestamp; float adc_multiplier; char owner_info[120]; + uint8_t direct_retry_attempts; + uint16_t direct_retry_base_ms; + uint8_t direct_retry_timing_magic[2]; }; class CommonCLICallbacks { diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index f5da272b1b..705869ad35 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -279,6 +279,51 @@ class SimpleMeshTables : public mesh::MeshTables { } return NULL; } + int getRecentRepeaterCount() const { + int count = 0; + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + if (_recent_repeaters[i].prefix_len > 0) { + count++; + } + } + return count; + } + const RecentRepeaterInfo* getRecentRepeaterNewestByIdx(int idx_wanted) const { + if (idx_wanted < 0) { + return NULL; + } + int idx_seen = 0; + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; + const RecentRepeaterInfo* info = &_recent_repeaters[idx]; + if (info->prefix_len == 0) { + continue; + } + if (idx_seen == idx_wanted) { + return info; + } + idx_seen++; + } + return NULL; + } + const RecentRepeaterInfo* getRecentRepeaterOldestByIdx(int idx_wanted) const { + if (idx_wanted < 0) { + return NULL; + } + int idx_seen = 0; + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx + i) % MAX_RECENT_REPEATERS; + const RecentRepeaterInfo* info = &_recent_repeaters[idx]; + if (info->prefix_len == 0) { + continue; + } + if (idx_seen == idx_wanted) { + return info; + } + idx_seen++; + } + return NULL; + } const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t* hash, uint8_t hash_len) const { if (hash == NULL || hash_len == 0) { From d028c69d46d9807495173b6e78a7b7cddc4ab53e Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 24 Apr 2026 14:41:07 -0700 Subject: [PATCH 66/94] Fix issue with packet prefixes getting added to the table. --- docs/cli_commands.md | 6 +- examples/simple_repeater/MyMesh.cpp | 93 ++++++++++++++++++++++------- src/helpers/SimpleMeshTables.h | 24 ++++---- 3 files changed, 91 insertions(+), 32 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index bbd2869340..14d68a0665 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -102,19 +102,23 @@ **Usage:** - `get recent.repeater` - `get recent.repeater all` +- `get recent.repeater all ` - `get recent.repeater first ` +- `get recent.repeater first ` - `get recent.repeater last ` +- `get recent.repeater last ` - `set recent.repeater ` **Parameters:** - `prefix_hex`: 1-3 bytes of next-hop prefix (hex) - `snr_db`: SNR in dB (supports decimals; stored at x4 precision) - `count`: number of entries to print +- `offset`: zero-based row offset into the selected order **Notes:** - `set` is rejected when the prefix already exists in neighbors. - `all` prints oldest to newest; `first` prints the oldest N; `last` prints the newest N. -- Remote CLI replies include rows too, but may truncate when the packet payload limit is reached. +- Over LoRa remote CLI, replies are packet-size limited; use `offset` to page through all rows. --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 3e8985e850..bf160b22b4 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1317,7 +1317,11 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply auto* tables = (SimpleMeshTables*)getTables(); - if (is_set || (!is_get && *sub != 0 && strcmp(sub, "all") != 0 && strncmp(sub, "first ", 6) != 0 && strncmp(sub, "last ", 5) != 0)) { + bool is_all = (strcmp(sub, "all") == 0 || strncmp(sub, "all ", 4) == 0); + bool is_first = (strncmp(sub, "first ", 6) == 0); + bool is_last = (strncmp(sub, "last ", 5) == 0); + + if (is_set || (!is_get && *sub != 0 && !is_all && !is_first && !is_last)) { char* params = (char*) sub; char* arg_snr = strchr(params, ' '); if (arg_snr == NULL) { @@ -1354,62 +1358,111 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply mesh::Utils::toHex(hex, info->prefix, info->prefix_len); sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); } - } else if (strcmp(sub, "all") == 0 || strncmp(sub, "first ", 6) == 0 || strncmp(sub, "last ", 5) == 0) { + } else if (is_all || is_first || is_last) { int total = tables->getRecentRepeaterCount(); if (total <= 0) { strcpy(reply, "> none"); } else { bool newest_first = false; int limit = total; + int offset = 0; const char* mode = "all"; - if (strncmp(sub, "first ", 6) == 0 || strncmp(sub, "last ", 5) == 0) { - const char* nstr = sub + (sub[0] == 'f' ? 6 : 5); + + if (is_first || is_last) { + const char* nstr = sub + (is_first ? 6 : 5); while (*nstr == ' ') nstr++; if (*nstr == 0) { - strcpy(reply, "Err - usage: get recent.repeater first|last "); + strcpy(reply, "Err - usage: get recent.repeater first|last [offset]"); return; } + char* end_ptr = NULL; - long parsed = strtol(nstr, &end_ptr, 10); + long parsed_count = strtol(nstr, &end_ptr, 10); while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; - if (end_ptr == NULL || *end_ptr != 0 || parsed <= 0) { + if (end_ptr == NULL || parsed_count <= 0) { strcpy(reply, "Err - count must be > 0"); return; } - limit = (int)parsed; - if (sub[0] == 'l') { + + if (*end_ptr != 0) { + char* end_ptr2 = NULL; + long parsed_offset = strtol(end_ptr, &end_ptr2, 10); + while (end_ptr2 != NULL && *end_ptr2 == ' ') end_ptr2++; + if (end_ptr2 == NULL || *end_ptr2 != 0 || parsed_offset < 0) { + strcpy(reply, "Err - offset must be >= 0"); + return; + } + offset = (int)parsed_offset; + } + + limit = (int)parsed_count; + if (is_last) { newest_first = true; mode = "last"; } else { mode = "first"; } + } else if (strncmp(sub, "all ", 4) == 0) { + const char* arg = sub + 4; + while (*arg == ' ') arg++; + if (*arg == 0) { + strcpy(reply, "Err - usage: get recent.repeater all "); + return; + } + + char* end_ptr = NULL; + long parsed_a = strtol(arg, &end_ptr, 10); + while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; + if (end_ptr == NULL || parsed_a <= 0) { + strcpy(reply, "Err - count must be > 0"); + return; + } + + char* end_ptr2 = NULL; + long parsed_b = strtol(end_ptr, &end_ptr2, 10); + while (end_ptr2 != NULL && *end_ptr2 == ' ') end_ptr2++; + if (end_ptr2 == NULL || *end_ptr2 != 0 || parsed_b < 0) { + strcpy(reply, "Err - usage: get recent.repeater all "); + return; + } + limit = (int)parsed_a; + offset = (int)parsed_b; } - if (limit > total) { - limit = total; + + if (offset >= total) { + sprintf(reply, "> none (%s off=%d/%d)", mode, offset, total); + return; + } + + int available = total - offset; + if (limit > available) { + limit = available; } if (sender_timestamp == 0) { - Serial.printf("Recent repeater table (%s %d/%d):\n", mode, limit, total); + Serial.printf("Recent repeater table (%s %d/%d, off=%d):\n", mode, limit, total, offset); for (int i = 0; i < limit; i++) { - const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(i) : tables->getRecentRepeaterOldestByIdx(i); + int idx = offset + i; + const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(idx) : tables->getRecentRepeaterOldestByIdx(idx); if (info == NULL) { continue; } char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - Serial.printf("%02d: %s,%s\n", i + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + Serial.printf("%02d: %s,%s\n", idx + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); } - sprintf(reply, "> showing %d/%d (%s)", limit, total, mode); + sprintf(reply, "> %s off=%d n=%d/%d", mode, offset, limit, total); } else { // Remote CLI replies are packet-bound, so include as many rows as fit. - int written = snprintf(reply, 160, "> showing %d/%d (%s)", limit, total, mode); + int written = snprintf(reply, 160, "> %s off=%d n=%d/%d", mode, offset, limit, total); bool truncated = false; if (written < 0) { reply[0] = 0; written = 0; } for (int i = 0; i < limit; i++) { - const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(i) : tables->getRecentRepeaterOldestByIdx(i); + int idx = offset + i; + const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(idx) : tables->getRecentRepeaterOldestByIdx(idx); if (info == NULL) { continue; } @@ -1419,7 +1472,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - int n = snprintf(reply + written, 160 - written, "\n%02d:%s,%s", i + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + int n = snprintf(reply + written, 160 - written, "\n%02d:%s,%s", idx + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); if (n < 0 || n >= (160 - written)) { truncated = true; break; @@ -1427,12 +1480,12 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply written += n; } if (truncated && written < 156) { - snprintf(reply + written, 160 - written, "\n..."); + snprintf(reply + written, 160 - written, "\n... use offset"); } } } } else { - strcpy(reply, "Err - usage: get recent.repeater [all|first |last ]"); + strcpy(reply, "Err - usage: get recent.repeater [all|all |first [offset]|last [offset]]"); } } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 705869ad35..539bd5b641 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -70,6 +70,19 @@ class SimpleMeshTables : public mesh::MeshTables { bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { // Learn repeater prefixes only from packet shapes that expose a trustworthy repeater ID. + // For flood traffic, the last path entry is the repeater we directly heard. + if (packet->isRouteFlood() && packet->getPathHashCount() > 0) { + prefix_len = packet->getPathHashSize(); + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + const uint8_t* last_hop = &packet->path[(packet->getPathHashCount() - 1) * packet->getPathHashSize()]; + memcpy(prefix, last_hop, prefix_len); + return true; + } + + // If there is no flood path to inspect, fall back to payload-derived identities. if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->payload_len >= PUB_KEY_SIZE) { memcpy(prefix, packet->payload, MAX_ROUTE_HASH_BYTES); prefix_len = MAX_ROUTE_HASH_BYTES; @@ -86,17 +99,6 @@ class SimpleMeshTables : public mesh::MeshTables { return true; } - if (packet->isRouteFlood() && packet->getPathHashCount() > 0) { - prefix_len = packet->getPathHashSize(); - if (prefix_len > MAX_ROUTE_HASH_BYTES) { - prefix_len = MAX_ROUTE_HASH_BYTES; - } - - const uint8_t* last_hop = &packet->path[(packet->getPathHashCount() - 1) * packet->getPathHashSize()]; - memcpy(prefix, last_hop, prefix_len); - return true; - } - return false; } From d1d50967cbc99c0204dc7b7cf4428ad77b276a9e Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 24 Apr 2026 15:19:48 -0700 Subject: [PATCH 67/94] Round up the SNR vs replacement to get a weighted average. --- src/helpers/SimpleMeshTables.h | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 539bd5b641..effea219b5 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -68,6 +68,21 @@ class SimpleMeshTables : public mesh::MeshTables { return n > 0 && memcmp(a, b, n) == 0; } + int8_t avgSnrX4RoundUp(int8_t curr_snr_x4, int8_t new_snr_x4) const { + int16_t sum = (int16_t)curr_snr_x4 + (int16_t)new_snr_x4; + int16_t avg = sum / 2; // truncates toward zero + // "Round up" means ceil(), which only differs from truncation for positive odd sums. + if (sum > 0 && (sum & 1)) { + avg++; + } + if (avg > 127) { + avg = 127; + } else if (avg < -128) { + avg = -128; + } + return (int8_t)avg; + } + bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { // Learn repeater prefixes only from packet shapes that expose a trustworthy repeater ID. // For flood traffic, the last path entry is the repeater we directly heard. @@ -258,7 +273,7 @@ class SimpleMeshTables : public mesh::MeshTables { memcpy(existing.prefix, prefix, prefix_len); existing.prefix_len = prefix_len; } - existing.snr_x4 = snr_x4; + existing.snr_x4 = avgSnrX4RoundUp(existing.snr_x4, snr_x4); return true; } From 399939f41babbe7cacbdae95c33daa9fd0f95d14 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 27 Apr 2026 16:33:39 -0700 Subject: [PATCH 68/94] Refine direct retry SNR handling and recent repeater controls --- docs/cli_commands.md | 28 +- examples/simple_repeater/MyMesh.cpp | 434 ++++++++++++++++++++-------- examples/simple_repeater/MyMesh.h | 1 + src/Mesh.cpp | 15 +- src/helpers/CommonCLI.cpp | 28 +- src/helpers/CommonCLI.h | 2 +- src/helpers/SimpleMeshTables.h | 194 +++++++++++-- 7 files changed, 531 insertions(+), 171 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 14d68a0665..78f8626d79 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -101,24 +101,20 @@ ### Get or set recent repeater fallback prefix/SNR **Usage:** - `get recent.repeater` -- `get recent.repeater all` -- `get recent.repeater all ` -- `get recent.repeater first ` -- `get recent.repeater first ` -- `get recent.repeater last ` -- `get recent.repeater last ` -- `set recent.repeater ` +- `get recent.repeater ` +- `get recent.repeater page ` +- `set recent.repeater ` **Parameters:** -- `prefix_hex`: 1-3 bytes of next-hop prefix (hex) +- `prefix_hex_6`: Exactly 3 bytes of next-hop prefix in hex (6 chars) - `snr_db`: SNR in dB (supports decimals; stored at x4 precision) -- `count`: number of entries to print -- `offset`: zero-based row offset into the selected order +- `page`: 1-based page number **Notes:** - `set` is rejected when the prefix already exists in neighbors. -- `all` prints oldest to newest; `first` prints the oldest N; `last` prints the newest N. -- Over LoRa remote CLI, replies are packet-size limited; use `offset` to page through all rows. +- Rows are shown newest-first. +- Serial CLI prints all rows (no paging). +- Over LoRa remote CLI, page size is fixed at `4` rows; choose page with `get recent.repeater `. --- @@ -437,7 +433,7 @@ **Parameters:** - `state`: `on`|`off` -**Default:** `off` +**Default:** `on` **Note:** When enabled, a repeater can use recently-heard non-duplicate repeater prefixes as a fallback for direct retry eligibility when no suitable neighbor entry is available. @@ -449,9 +445,9 @@ - `set direct.retry.margin ` **Parameters:** -- `value`: Margin in dB above the SF-specific receive floor (minimum `0`, default `5`) +- `value`: Margin in dB above the SF-specific receive floor (minimum `0`, maximum `40`, quarter-dB precision, default `2.5`) -**Default:** `5` +**Default:** `2.5` **Note:** The retry gate uses the active SF floor of `SF5=-2.5`, `SF6=-5`, `SF7=-7.5`, `SF8=-10`, `SF9=-12.5`, `SF10=-15`, `SF11=-17.5`, `SF12=-20`, then adds this margin. @@ -465,7 +461,7 @@ **Parameters:** - `value`: Retry attempts after initial TX (`1`-`15`) -**Default:** `3` +**Default:** `15` --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index bf160b22b4..10be221264 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -86,6 +86,64 @@ bool MyMesh::allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefi return self->findNeighbourByHash(prefix, prefix_len) == NULL; } +static void formatRecentRepeaterPrefix(const SimpleMeshTables::RecentRepeaterInfo* info, char* out, size_t out_len) { + if (out == NULL || out_len == 0) { + return; + } + out[0] = 0; + if (info == NULL) { + return; + } + + uint8_t prefix_len = info->prefix_len; + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + if (prefix_len > 0) { + mesh::Utils::toHex(out, info->prefix, prefix_len); + } + + size_t used = strlen(out); + const size_t target_len = MAX_ROUTE_HASH_BYTES * 2; + while (used < target_len && used + 1 < out_len) { + out[used++] = ' '; + } + out[used] = 0; +} + +static void formatRecentRepeaterSnrX4(int8_t snr_x4, char* out, size_t out_len) { + if (out == NULL || out_len == 0) { + return; + } + + const char* snr_text = StrHelper::ftoa(((float)snr_x4) / 4.0f); + if (snr_text[0] == '-') { + snprintf(out, out_len, "%s", snr_text); + } else { + snprintf(out, out_len, " %s", snr_text); + } +} + +static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) { + uint8_t code = flags & 0x03; + uint8_t size_pow2 = (uint8_t)(1U << code); // legacy TRACE interpretation + uint8_t size_linear = (uint8_t)(code + 1U); // packed-size interpretation (1..4) + + bool pow2_ok = size_pow2 > 0 && (route_bytes % size_pow2) == 0; + bool linear_ok = size_linear > 0 && (route_bytes % size_linear) == 0; + + if (pow2_ok && !linear_ok) { + return size_pow2; + } + if (linear_ok && !pow2_ok) { + return size_linear; + } + if (pow2_ok) { + return size_pow2; + } + return size_linear; +} + void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { #if MAX_NEIGHBOURS // check if neighbours enabled // find existing neighbour, else use least recently updated @@ -430,7 +488,31 @@ bool MyMesh::isLooped(const mesh::Packet* packet, const uint8_t max_counters[]) bool MyMesh::allowPacketForward(const mesh::Packet *packet) { if (_prefs.disable_fwd) return false; - if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false; + + if (packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE && packet->payload_len >= 9) { + auto* tables = (SimpleMeshTables *)getTables(); + uint8_t route_bytes = packet->payload_len - 9; + uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes); + uint16_t offset = (uint16_t)packet->path_len * (uint16_t)hash_size; + uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); + int16_t fallback_snr_x4 = direct_retry_floor_x4[sf - 5] + 40; // fixed +10 dB above SF floor + + // A successful TRACE forward reveals the downstream next-hop hash. Seed/update the recent table immediately. + if (hash_size > 0 && offset + (2U * hash_size) <= route_bytes) { + uint8_t prefix_len = hash_size; + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + const uint8_t* next_hop_prefix = &packet->payload[9 + offset + hash_size]; + const auto* existing = tables->findRecentRepeaterByHash(next_hop_prefix, prefix_len); + // This point only proves we can forward TO next_hop; packet->_snr is upstream RX and not a + // trustworthy metric for next_hop. Seed with existing table value or fallback only. + int8_t trace_snr_x4 = (existing != NULL) ? existing->snr_x4 : (int8_t)constrain(fallback_snr_x4, -128, 127); + tables->setRecentRepeater(next_hop_prefix, prefix_len, trace_snr_x4, false, true); + } + } + + if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; if (packet->isRouteFlood() && recv_pkt_region == NULL) { MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet"); return false; @@ -524,23 +606,102 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u return; } - MESH_DEBUG_PRINTLN("%s direct retry %s (type=%d, route=%s, payload_len=%d, delay=%lu)", + uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; + uint8_t prefix_len = 0; + bool has_prefix = extractDirectRetryPrefix(packet, prefix, prefix_len); + auto* tables = (SimpleMeshTables *)getTables(); + const auto* existing = has_prefix ? tables->findRecentRepeaterByHash(prefix, prefix_len) : NULL; + char next_hop_hex[(MAX_ROUTE_HASH_BYTES * 2) + 1] = {0}; + if (has_prefix && prefix_len > 0) { + mesh::Utils::toHex(next_hop_hex, prefix, prefix_len); + } + const char* next_hop = (has_prefix && prefix_len > 0) ? next_hop_hex : "unknown"; + // Direct-retry events are TX-side and usually have no trustworthy RX SNR. + // Cap event SNR at fixed SF floor + 10 dB so trace-start retries can't inflate table SNR. + uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); + int16_t fallback_snr_x4_raw = direct_retry_floor_x4[sf - 5] + 40; + int8_t fallback_snr_x4 = (int8_t)constrain(fallback_snr_x4_raw, -128, 127); + bool is_success_event = (strcmp(event, "good") == 0 || strcmp(event, "canceled_echo") == 0); + int8_t retry_event_snr_x4; + const char* snr_src; + if (is_success_event && packet->_snr != 0) { + // On success, Mesh.cpp injects echo RX SNR for TRACE retries. + retry_event_snr_x4 = packet->_snr; + snr_src = "packet"; + } else if (existing != NULL) { + retry_event_snr_x4 = existing->snr_x4; + snr_src = "table"; + } else { + retry_event_snr_x4 = fallback_snr_x4; + snr_src = "fallback"; + } + char snr_used_text[12]; + char snr_pkt_text[12]; + char snr_table_text[12]; + snprintf(snr_used_text, sizeof(snr_used_text), "%s", StrHelper::ftoa(((float)retry_event_snr_x4) / 4.0f)); + snprintf(snr_pkt_text, sizeof(snr_pkt_text), "%s", StrHelper::ftoa(((float)packet->_snr) / 4.0f)); + if (existing != NULL) { + snprintf(snr_table_text, sizeof(snr_table_text), "%s", StrHelper::ftoa(((float)existing->snr_x4) / 4.0f)); + } else { + snprintf(snr_table_text, sizeof(snr_table_text), "na"); + } + + if (has_prefix && is_success_event) { + // Refresh SNR only on successful echo/progress events, not on queued/resent bookkeeping. + tables->setRecentRepeater(prefix, prefix_len, retry_event_snr_x4, false, true); + } + + if (strcmp(event, "resent") == 0) { + if (has_prefix) { + // Retry stats should be visible even when the prefix was never learned into recent.repeater. + tables->incrementRecentRepeaterRetryCount(prefix, prefix_len, true, retry_event_snr_x4, true); + } + } else if (strcmp(event, "failed_all_tries") == 0) { + if (has_prefix) { + // A failed_all_tries event means all retry attempts for this packet failed. + // Count failures by retry-attempts so fail% reflects failed retries, not just failed sessions. + uint8_t give_up_retries = getDirectRetryMaxAttempts(packet); + uint8_t failed_retries = give_up_retries; + if (failed_retries < 1) { + failed_retries = 1; + } + for (uint8_t i = 0; i < failed_retries; i++) { + tables->incrementRecentRepeaterFailCount(prefix, prefix_len, true, retry_event_snr_x4, true); + } + if (failed_retries >= give_up_retries && give_up_retries > 0) { + // If all configured retry attempts still fail, slightly degrade stored path quality. + tables->decrementRecentRepeaterSnrX4(prefix, prefix_len, 1); + } + } + } + + MESH_DEBUG_PRINTLN("%s direct retry %s (type=%d, route=%s, payload_len=%d, next_hop=%s, snr=%s, snr_src=%s, pkt_snr=%s, table_snr=%s, delay=%lu)", getLogDateTime(), event, (uint32_t)packet->getPayloadType(), packet->isRouteDirect() ? "D" : "F", (uint32_t)packet->payload_len, + next_hop, + snr_used_text, + snr_src, + snr_pkt_text, + snr_table_text, (unsigned long)delay_millis); if (_logging) { File f = openAppend(PACKET_LOG_FILE); if (f) { f.print(getLogDateTime()); - f.printf(": DIRECT RETRY %s (type=%d, route=%s, payload_len=%d, delay=%lu)\n", + f.printf(": DIRECT RETRY %s (type=%d, route=%s, payload_len=%d, next_hop=%s, snr=%s, snr_src=%s, pkt_snr=%s, table_snr=%s, delay=%lu)\n", event, (uint32_t)packet->getPayloadType(), packet->isRouteDirect() ? "D" : "F", (uint32_t)packet->payload_len, + next_hop, + snr_used_text, + snr_src, + snr_pkt_text, + snr_table_text, (unsigned long)delay_millis); f.close(); } @@ -563,28 +724,59 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { int8_t MyMesh::getDirectRetryMinSNRX4() const { // Use the live SF so `tempradio` changes immediately affect the retry threshold. uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); - int16_t threshold = direct_retry_floor_x4[sf - 5] + ((int16_t)_prefs.direct_retry_snr_margin_db * 4); + int16_t threshold = direct_retry_floor_x4[sf - 5] + (int16_t)_prefs.direct_retry_snr_margin_db; return (int8_t)constrain(threshold, -128, 127); } -bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { - if (_prefs.disable_fwd) { +bool MyMesh::extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { + if (packet == NULL || prefix == NULL) { return false; } - int8_t min_snr_x4 = getDirectRetryMinSNRX4(); - const NeighbourInfo* neighbour = findNeighbourByHash(next_hop_hash, next_hop_hash_len); - // Prefer the explicit neighbor table first; it is the strongest signal that this hop is still reachable. - if (neighbour != NULL && neighbour->snr >= min_snr_x4) { + // TRACE direct routes encode repeater hashes in payload; packet->path carries SNR trail bytes. + if (packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE && packet->payload_len >= 9) { + uint8_t route_bytes = packet->payload_len - 9; + uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes); + uint8_t offset = packet->path_len * hash_size; + if (hash_size > 0 && offset + hash_size <= route_bytes) { + prefix_len = hash_size; + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + memcpy(prefix, &packet->payload[9 + offset], prefix_len); + return true; + } + } + + if (packet->isRouteDirect() && packet->getPathHashCount() > 0) { + prefix_len = packet->getPathHashSize(); + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + if (prefix_len == 0) { + return false; + } + memcpy(prefix, packet->path, prefix_len); return true; } - if (!_prefs.direct_retry_recent_enabled) { + return false; +} +bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { + if (_prefs.disable_fwd) { return false; } - // If no neighbor entry exists, fall back to the recent-heard repeater cache keyed by the same path prefix. - const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len); - return recent != NULL && recent->snr_x4 >= min_snr_x4; + int8_t min_snr_x4 = getDirectRetryMinSNRX4(); + if (_prefs.direct_retry_recent_enabled) { + // Prefer the 64-entry recent-prefix cache first, then fall back to neighbours. + const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len); + if (recent != NULL && recent->snr_x4 >= min_snr_x4) { + return true; + } + } + + const NeighbourInfo* neighbour = findNeighbourByHash(next_hop_hash, next_hop_hash_len); + return neighbour != NULL && neighbour->snr >= min_snr_x4; } uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { uint32_t base_wait_millis = constrain((uint32_t)_prefs.direct_retry_base_ms, (uint32_t)10, (uint32_t)5000); @@ -888,9 +1080,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.tx_delay_factor = 0.5f; // was 0.25f _prefs.direct_tx_delay_factor = 0.3f; // was 0.2 - _prefs.direct_retry_recent_enabled = 0; - _prefs.direct_retry_snr_margin_db = 5; - _prefs.direct_retry_attempts = 3; + _prefs.direct_retry_recent_enabled = 1; + _prefs.direct_retry_snr_margin_db = 10; // 2.5 dB stored in x4 units + _prefs.direct_retry_attempts = 15; _prefs.direct_retry_base_ms = 200; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; @@ -1299,9 +1491,11 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } else if (strncmp(command, "get recent.repeater", 19) == 0 || strncmp(command, "set recent.repeater", 19) == 0 + || strncmp(command, "clear recent.repeater", 21) == 0 || strncmp(command, "recent.repeater", 15) == 0) { bool is_get = false; bool is_set = false; + bool is_clear = false; const char* sub = command; if (strncmp(command, "get recent.repeater", 19) == 0) { @@ -1310,38 +1504,55 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else if (strncmp(command, "set recent.repeater", 19) == 0) { is_set = true; sub = command + 19; + } else if (strncmp(command, "clear recent.repeater", 21) == 0) { + is_clear = true; + sub = command + 21; } else { sub = command + 15; // legacy command format } while (*sub == ' ') sub++; auto* tables = (SimpleMeshTables*)getTables(); + if (!is_get && !is_set && !is_clear && strncmp(sub, "clear", 5) == 0 && (sub[5] == 0 || sub[5] == ' ')) { + is_clear = true; + sub += 5; + while (*sub == ' ') sub++; + } - bool is_all = (strcmp(sub, "all") == 0 || strncmp(sub, "all ", 4) == 0); - bool is_first = (strncmp(sub, "first ", 6) == 0); - bool is_last = (strncmp(sub, "last ", 5) == 0); - - if (is_set || (!is_get && *sub != 0 && !is_all && !is_first && !is_last)) { + if (is_clear) { + if (*sub != 0) { + strcpy(reply, "Err - usage: clear recent.repeater"); + } else { + tables->clearRecentRepeaters(); + strcpy(reply, "OK"); + } + } else if (is_set) { char* params = (char*) sub; char* arg_snr = strchr(params, ' '); if (arg_snr == NULL) { - strcpy(reply, "Err - usage: set recent.repeater "); + strcpy(reply, "Err - usage: set recent.repeater "); } else { *arg_snr++ = 0; while (*arg_snr == ' ') arg_snr++; if (*arg_snr == 0) { - strcpy(reply, "Err - usage: set recent.repeater "); + strcpy(reply, "Err - usage: set recent.repeater "); } else { - int hex_len = strlen(params); - int prefix_len = hex_len / 2; uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; - if ((hex_len % 2) != 0 || prefix_len <= 0 || prefix_len > MAX_ROUTE_HASH_BYTES || !mesh::Utils::fromHex(prefix, prefix_len, params)) { - strcpy(reply, "Err - prefix must be 1-3 bytes hex"); + int hex_len = strlen(params); + if (hex_len != (MAX_ROUTE_HASH_BYTES * 2) || !mesh::Utils::fromHex(prefix, MAX_ROUTE_HASH_BYTES, params)) { + strcpy(reply, "Err - prefix must be exactly 3 bytes hex (6 chars)"); } else { - float snr_db = strtof(arg_snr, nullptr); + char* end_snr = NULL; + float snr_db = strtof(arg_snr, &end_snr); + while (end_snr != NULL && *end_snr == ' ') end_snr++; + if (end_snr == arg_snr || (end_snr != NULL && *end_snr != 0)) { + strcpy(reply, "Err - snr must be numeric"); + return; + } + int snr_x4 = (int)(snr_db * 4.0f + (snr_db >= 0.0f ? 0.5f : -0.5f)); snr_x4 = constrain(snr_x4, -128, 127); - if (tables->setRecentRepeater(prefix, (uint8_t)prefix_len, (int8_t)snr_x4)) { + if (tables->setRecentRepeater(prefix, MAX_ROUTE_HASH_BYTES, (int8_t)snr_x4, true)) { strcpy(reply, "OK"); } else { strcpy(reply, "Err - prefix is already in neighbors"); @@ -1349,120 +1560,82 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } } - } else if (*sub == 0) { - const auto* info = tables->getLatestRecentRepeater(); - if (info == NULL) { - strcpy(reply, "> none"); - } else { - char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; - mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); - } - } else if (is_all || is_first || is_last) { + } else { int total = tables->getRecentRepeaterCount(); if (total <= 0) { strcpy(reply, "> none"); } else { - bool newest_first = false; - int limit = total; - int offset = 0; - const char* mode = "all"; - - if (is_first || is_last) { - const char* nstr = sub + (is_first ? 6 : 5); - while (*nstr == ' ') nstr++; - if (*nstr == 0) { - strcpy(reply, "Err - usage: get recent.repeater first|last [offset]"); - return; - } + if (sender_timestamp == 0) { + // Serial CLI: print all entries (no paging). + Serial.printf("Recent repeater table (newest first, total=%d):\n", total); + for (int i = 0; i < total; i++) { + const auto* info = tables->getRecentRepeaterNewestByIdx(i); + if (info == NULL) { + continue; + } - char* end_ptr = NULL; - long parsed_count = strtol(nstr, &end_ptr, 10); - while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; - if (end_ptr == NULL || parsed_count <= 0) { - strcpy(reply, "Err - count must be > 0"); - return; + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; + formatRecentRepeaterPrefix(info, hex, sizeof(hex)); + char snr_text[12]; + formatRecentRepeaterSnrX4(info->snr_x4, snr_text, sizeof(snr_text)); + uint32_t fail_pct_x10 = 0; + if (info->retry_count > 0) { + fail_pct_x10 = (((uint32_t)info->fail_count * 1000UL) + (info->retry_count / 2U)) / (uint32_t)info->retry_count; + } + Serial.printf("%03d: %s,%s,fp=%lu.%01lu%%,r=%u,f=%u%s\n", + i + 1, + hex, + snr_text, + (unsigned long)(fail_pct_x10 / 10U), + (unsigned long)(fail_pct_x10 % 10U), + (uint32_t)info->retry_count, + (uint32_t)info->fail_count, + info->snr_locked ? ",l" : ""); + } + sprintf(reply, "> n=%d/%d", total, total); + } else { + // Remote CLI: page by fixed size to fit packet-limited reply payload. + long page_num = 1; + const long page_size = 4; + const char* arg = sub; + + if (strncmp(arg, "page ", 5) == 0) { + arg += 5; + while (*arg == ' ') arg++; } - if (*end_ptr != 0) { - char* end_ptr2 = NULL; - long parsed_offset = strtol(end_ptr, &end_ptr2, 10); - while (end_ptr2 != NULL && *end_ptr2 == ' ') end_ptr2++; - if (end_ptr2 == NULL || *end_ptr2 != 0 || parsed_offset < 0) { - strcpy(reply, "Err - offset must be >= 0"); + if (*arg != 0) { + char* end_ptr = NULL; + page_num = strtol(arg, &end_ptr, 10); + while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; + if (end_ptr == NULL || page_num <= 0 || (end_ptr != NULL && *end_ptr != 0)) { + strcpy(reply, "Err - usage: get recent.repeater [page]"); return; } - offset = (int)parsed_offset; - } - - limit = (int)parsed_count; - if (is_last) { - newest_first = true; - mode = "last"; - } else { - mode = "first"; - } - } else if (strncmp(sub, "all ", 4) == 0) { - const char* arg = sub + 4; - while (*arg == ' ') arg++; - if (*arg == 0) { - strcpy(reply, "Err - usage: get recent.repeater all "); - return; } - char* end_ptr = NULL; - long parsed_a = strtol(arg, &end_ptr, 10); - while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; - if (end_ptr == NULL || parsed_a <= 0) { - strcpy(reply, "Err - count must be > 0"); + int total_pages = (total + (int)page_size - 1) / (int)page_size; + if (page_num > total_pages) { + sprintf(reply, "> none (page=%ld/%d)", page_num, total_pages); return; } - char* end_ptr2 = NULL; - long parsed_b = strtol(end_ptr, &end_ptr2, 10); - while (end_ptr2 != NULL && *end_ptr2 == ' ') end_ptr2++; - if (end_ptr2 == NULL || *end_ptr2 != 0 || parsed_b < 0) { - strcpy(reply, "Err - usage: get recent.repeater all "); - return; + int offset = ((int)page_num - 1) * (int)page_size; + int limit = total - offset; + if (limit > (int)page_size) { + limit = (int)page_size; } - limit = (int)parsed_a; - offset = (int)parsed_b; - } - - if (offset >= total) { - sprintf(reply, "> none (%s off=%d/%d)", mode, offset, total); - return; - } - int available = total - offset; - if (limit > available) { - limit = available; - } - - if (sender_timestamp == 0) { - Serial.printf("Recent repeater table (%s %d/%d, off=%d):\n", mode, limit, total, offset); - for (int i = 0; i < limit; i++) { - int idx = offset + i; - const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(idx) : tables->getRecentRepeaterOldestByIdx(idx); - if (info == NULL) { - continue; - } - char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; - mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - Serial.printf("%02d: %s,%s\n", idx + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); - } - sprintf(reply, "> %s off=%d n=%d/%d", mode, offset, limit, total); - } else { - // Remote CLI replies are packet-bound, so include as many rows as fit. - int written = snprintf(reply, 160, "> %s off=%d n=%d/%d", mode, offset, limit, total); + int written = snprintf(reply, 160, "> page=%ld/%d n=%d/%d", page_num, total_pages, limit, total); bool truncated = false; if (written < 0) { reply[0] = 0; written = 0; } + for (int i = 0; i < limit; i++) { int idx = offset + i; - const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(idx) : tables->getRecentRepeaterOldestByIdx(idx); + const auto* info = tables->getRecentRepeaterNewestByIdx(idx); if (info == NULL) { continue; } @@ -1470,9 +1643,20 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply truncated = true; break; } + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; - mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - int n = snprintf(reply + written, 160 - written, "\n%02d:%s,%s", idx + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + formatRecentRepeaterPrefix(info, hex, sizeof(hex)); + char snr_text[12]; + formatRecentRepeaterSnrX4(info->snr_x4, snr_text, sizeof(snr_text)); + int n = snprintf(reply + written, + 160 - written, + "\n%03d:%s,%s,r=%u,f=%u%s", + idx + 1, + hex, + snr_text, + (uint32_t)info->retry_count, + (uint32_t)info->fail_count, + info->snr_locked ? ",l" : ""); if (n < 0 || n >= (160 - written)) { truncated = true; break; @@ -1480,12 +1664,10 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply written += n; } if (truncated && written < 156) { - snprintf(reply + written, 160 - written, "\n... use offset"); + snprintf(reply + written, 160 - written, "\n... next page"); } } } - } else { - strcpy(reply, "Err - usage: get recent.repeater [all|all |first [offset]|last [offset]]"); } } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 3ecbfc81e0..d873a6b446 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -119,6 +119,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { #endif const NeighbourInfo* findNeighbourByHash(const uint8_t* hash, uint8_t hash_len) const; + bool extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const; static bool allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefix_len, void* ctx); int8_t getDirectRetryMinSNRX4() const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); diff --git a/src/Mesh.cpp b/src/Mesh.cpp index be2c81f1a0..bc34c5eed2 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -3,7 +3,7 @@ namespace mesh { -static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 3; +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 15; static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; void Mesh::begin() { @@ -498,6 +498,12 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { } if (_direct_retries[i].queued) { + if (_direct_retries[i].expect_path_growth + && _direct_retries[i].packet != NULL + && _direct_retries[i].progress_marker < packet->path_len) { + // For retry-good quality, use the received echo packet SNR (return-link quality). + _direct_retries[i].packet->_snr = packet->_snr; + } for (int j = 0; j < _mgr->getOutboundTotal(); j++) { if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { Packet* pending = _mgr->removeOutboundByIdx(j); @@ -511,6 +517,12 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { onDirectRetryEvent("good", _direct_retries[i].packet, 0); clearDirectRetrySlot(i); } else { + if (_direct_retries[i].expect_path_growth + && _direct_retries[i].trigger_packet != NULL + && _direct_retries[i].progress_marker < packet->path_len) { + // For retry-good quality, use the received echo packet SNR (return-link quality). + _direct_retries[i].trigger_packet->_snr = packet->_snr; + } onDirectRetryEvent("canceled_echo", _direct_retries[i].trigger_packet, 0); onDirectRetryEvent("good", _direct_retries[i].trigger_packet, 0); clearDirectRetrySlot(i); @@ -539,6 +551,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX; } if (_direct_retries[i].retry_attempts_sent >= max_attempts) { + onDirectRetryEvent("failed_all_tries", packet, 0); onDirectRetryEvent("failure", packet, 0); clearDirectRetrySlot(i); continue; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index bae389ba55..03d83308ac 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -11,12 +11,13 @@ // These bytes used to be reserved/unused in persisted prefs, so keep a marker before trusting them. #define DIRECT_RETRY_PREFS_MAGIC_0 0xD4 #define DIRECT_RETRY_PREFS_MAGIC_1 0x52 -#define DIRECT_RETRY_RECENT_DEFAULT 0 -#define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT 5 -#define DIRECT_RETRY_SNR_MARGIN_DB_MAX 40 +#define DIRECT_RETRY_RECENT_DEFAULT 1 +#define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT_X4 10 +#define DIRECT_RETRY_SNR_MARGIN_DB_MAX 40 +#define DIRECT_RETRY_SNR_MARGIN_X4_MAX (DIRECT_RETRY_SNR_MARGIN_DB_MAX * 4) #define DIRECT_RETRY_TIMING_MAGIC_0 0xD5 #define DIRECT_RETRY_TIMING_MAGIC_1 0x54 -#define DIRECT_RETRY_COUNT_DEFAULT 3 +#define DIRECT_RETRY_COUNT_DEFAULT 15 #define DIRECT_RETRY_COUNT_MIN 1 #define DIRECT_RETRY_COUNT_MAX 15 #define DIRECT_RETRY_BASE_MS_DEFAULT 200 @@ -33,6 +34,15 @@ static uint32_t _atoi(const char* sp) { return n; } +static uint8_t directRetryMarginDbToX4(float margin_db) { + int32_t scaled_x4 = (int32_t)((margin_db * 4.0f) + 0.5f); + return (uint8_t)constrain(scaled_x4, 0, DIRECT_RETRY_SNR_MARGIN_X4_MAX); +} + +static float directRetryMarginX4ToDb(uint8_t margin_x4) { + return ((float)margin_x4) / 4.0f; +} + static bool isValidName(const char *n) { while (*n) { if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false; @@ -123,10 +133,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { if (_prefs->direct_retry_prefs_magic[0] != DIRECT_RETRY_PREFS_MAGIC_0 || _prefs->direct_retry_prefs_magic[1] != DIRECT_RETRY_PREFS_MAGIC_1) { _prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT; - _prefs->direct_retry_snr_margin_db = DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT; + _prefs->direct_retry_snr_margin_db = DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT_X4; } else { _prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1); - _prefs->direct_retry_snr_margin_db = constrain(_prefs->direct_retry_snr_margin_db, 0, DIRECT_RETRY_SNR_MARGIN_DB_MAX); + _prefs->direct_retry_snr_margin_db = constrain(_prefs->direct_retry_snr_margin_db, 0, DIRECT_RETRY_SNR_MARGIN_X4_MAX); } if (_prefs->direct_retry_timing_magic[0] != DIRECT_RETRY_TIMING_MAGIC_0 || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1) { @@ -368,7 +378,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else if (memcmp(config, "direct.retry.heard", 18) == 0) { sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); } else if (memcmp(config, "direct.retry.margin", 19) == 0) { - sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_snr_margin_db); + sprintf(reply, "> %s", StrHelper::ftoa(directRetryMarginX4ToDb(_prefs->direct_retry_snr_margin_db))); } else if (memcmp(config, "direct.retry.count", 18) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts); } else if (memcmp(config, "direct.retry.base", 17) == 0) { @@ -605,9 +615,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch strcpy(reply, "Error, must be on or off"); } } else if (memcmp(config, "direct.retry.margin ", 20) == 0) { - int db = atoi(&config[20]); + float db = atof(&config[20]); if (db >= 0 && db <= DIRECT_RETRY_SNR_MARGIN_DB_MAX) { - _prefs->direct_retry_snr_margin_db = (uint8_t)db; + _prefs->direct_retry_snr_margin_db = directRetryMarginDbToX4(db); savePrefs(); strcpy(reply, "OK"); } else { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index ab8e058a51..6d49fec8b1 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -28,7 +28,7 @@ struct NodePrefs { // persisted to file char guest_password[16]; float direct_tx_delay_factor; uint8_t direct_retry_recent_enabled; - uint8_t direct_retry_snr_margin_db; + uint8_t direct_retry_snr_margin_db; // stored in quarter-dB units (x4) uint8_t direct_retry_prefs_magic[2]; uint8_t sf; uint8_t cr; diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index effea219b5..ac6d01b57b 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -8,7 +8,14 @@ #define MAX_PACKET_HASHES 128 #define MAX_PACKET_ACKS 64 -#define MAX_RECENT_REPEATERS 64 +#ifndef MAX_RECENT_REPEATERS + // Two defaults. Can be overridden with -D MAX_RECENT_REPEATERS=. + #if defined(ESP32) + #define MAX_RECENT_REPEATERS 512 + #else + #define MAX_RECENT_REPEATERS 64 + #endif +#endif #define MAX_ROUTE_HASH_BYTES 3 class SimpleMeshTables : public mesh::MeshTables { @@ -17,9 +24,12 @@ class SimpleMeshTables : public mesh::MeshTables { struct RecentRepeaterInfo { // Just enough identity to match a next-hop path prefix plus the SNR that heard it. + uint16_t retry_count; + uint16_t fail_count; uint8_t prefix[MAX_ROUTE_HASH_BYTES]; uint8_t prefix_len; int8_t snr_x4; + uint8_t snr_locked; }; private: @@ -68,19 +78,20 @@ class SimpleMeshTables : public mesh::MeshTables { return n > 0 && memcmp(a, b, n) == 0; } - int8_t avgSnrX4RoundUp(int8_t curr_snr_x4, int8_t new_snr_x4) const { - int16_t sum = (int16_t)curr_snr_x4 + (int16_t)new_snr_x4; - int16_t avg = sum / 2; // truncates toward zero - // "Round up" means ceil(), which only differs from truncation for positive odd sums. - if (sum > 0 && (sum & 1)) { - avg++; + int8_t weightedSnrX4RoundUp(int8_t curr_snr_x4, int8_t new_snr_x4) const { + // Keep existing SNR heavier than a single new sample: 75% existing + 25% new. + int16_t weighted_sum = ((int16_t)curr_snr_x4 * 3) + (int16_t)new_snr_x4; + int16_t blended = weighted_sum / 4; // truncates toward zero + // "Round up" means ceil(), which only differs from truncation for positive remainders. + if (weighted_sum > 0 && (weighted_sum % 4) != 0) { + blended++; } - if (avg > 127) { - avg = 127; - } else if (avg < -128) { - avg = -128; + if (blended > 127) { + blended = 127; + } else if (blended < -128) { + blended = -128; } - return (int8_t)avg; + return (int8_t)blended; } bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { @@ -249,7 +260,8 @@ class SimpleMeshTables : public mesh::MeshTables { _recent_repeater_allow_fn = fn; _recent_repeater_allow_ctx = ctx; } - bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) { + bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4, bool snr_locked = false, + bool bypass_allow_filter = false) { if (prefix == NULL || prefix_len == 0) { return false; } @@ -258,7 +270,8 @@ class SimpleMeshTables : public mesh::MeshTables { prefix_len = MAX_ROUTE_HASH_BYTES; } - if (_recent_repeater_allow_fn != NULL && !_recent_repeater_allow_fn(prefix, prefix_len, _recent_repeater_allow_ctx)) { + if (!bypass_allow_filter && _recent_repeater_allow_fn != NULL + && !_recent_repeater_allow_fn(prefix, prefix_len, _recent_repeater_allow_ctx)) { return false; } @@ -273,19 +286,160 @@ class SimpleMeshTables : public mesh::MeshTables { memcpy(existing.prefix, prefix, prefix_len); existing.prefix_len = prefix_len; } - existing.snr_x4 = avgSnrX4RoundUp(existing.snr_x4, snr_x4); + if (snr_locked) { + existing.snr_x4 = snr_x4; + existing.snr_locked = 1; + } else if (!existing.snr_locked) { + existing.snr_x4 = weightedSnrX4RoundUp(existing.snr_x4, snr_x4); + } return true; } - // Ring buffer is enough here; retry fallback only needs a recent prefix->SNR observation. - RecentRepeaterInfo& slot = _recent_repeaters[_next_recent_repeater_idx]; + int slot_idx = -1; + // Prefer empty slots first while preserving newest-order iteration. + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx + i) % MAX_RECENT_REPEATERS; + if (_recent_repeaters[idx].prefix_len == 0) { + slot_idx = idx; + break; + } + } + if (slot_idx < 0) { + // Table is full: evict the weakest observed SNR entry. + slot_idx = 0; + int8_t min_snr_x4 = _recent_repeaters[0].snr_x4; + for (int i = 1; i < MAX_RECENT_REPEATERS; i++) { + if (_recent_repeaters[i].snr_x4 < min_snr_x4) { + min_snr_x4 = _recent_repeaters[i].snr_x4; + slot_idx = i; + } + } + } + + RecentRepeaterInfo& slot = _recent_repeaters[slot_idx]; memset(slot.prefix, 0, sizeof(slot.prefix)); memcpy(slot.prefix, prefix, prefix_len); slot.prefix_len = prefix_len; slot.snr_x4 = snr_x4; - _next_recent_repeater_idx = (_next_recent_repeater_idx + 1) % MAX_RECENT_REPEATERS; + slot.retry_count = 0; + slot.fail_count = 0; + slot.snr_locked = snr_locked ? 1 : 0; + _next_recent_repeater_idx = (slot_idx + 1) % MAX_RECENT_REPEATERS; return true; } + bool incrementRecentRepeaterRetryCount(const uint8_t* prefix, uint8_t prefix_len, + bool create_if_missing = false, int8_t seed_snr_x4 = 0, + bool bypass_allow_filter = false) { + if (prefix == NULL || prefix_len == 0) { + return false; + } + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (prefix_len > existing.prefix_len) { + memset(existing.prefix, 0, sizeof(existing.prefix)); + memcpy(existing.prefix, prefix, prefix_len); + existing.prefix_len = prefix_len; + } + if (existing.retry_count < 0xFFFF) { + existing.retry_count++; + } + return true; + } + + if (!create_if_missing || !setRecentRepeater(prefix, prefix_len, seed_snr_x4, false, bypass_allow_filter)) { + return false; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (existing.retry_count < 0xFFFF) { + existing.retry_count++; + } + return true; + } + return false; + } + bool incrementRecentRepeaterFailCount(const uint8_t* prefix, uint8_t prefix_len, + bool create_if_missing = false, int8_t seed_snr_x4 = 0, + bool bypass_allow_filter = false) { + if (prefix == NULL || prefix_len == 0) { + return false; + } + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (prefix_len > existing.prefix_len) { + memset(existing.prefix, 0, sizeof(existing.prefix)); + memcpy(existing.prefix, prefix, prefix_len); + existing.prefix_len = prefix_len; + } + if (existing.fail_count < 0xFFFF) { + existing.fail_count++; + } + return true; + } + + if (!create_if_missing || !setRecentRepeater(prefix, prefix_len, seed_snr_x4, false, bypass_allow_filter)) { + return false; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (existing.fail_count < 0xFFFF) { + existing.fail_count++; + } + return true; + } + return false; + } + bool decrementRecentRepeaterSnrX4(const uint8_t* prefix, uint8_t prefix_len, uint8_t amount_x4 = 1) { + if (prefix == NULL || prefix_len == 0 || amount_x4 == 0) { + return false; + } + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (prefix_len > existing.prefix_len) { + memset(existing.prefix, 0, sizeof(existing.prefix)); + memcpy(existing.prefix, prefix, prefix_len); + existing.prefix_len = prefix_len; + } + if (!existing.snr_locked) { + int16_t lowered = (int16_t)existing.snr_x4 - (int16_t)amount_x4; + if (lowered < -128) { + lowered = -128; + } + existing.snr_x4 = (int8_t)lowered; + } + return true; + } + return false; + } const RecentRepeaterInfo* getLatestRecentRepeater() const { for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; @@ -360,6 +514,10 @@ class SimpleMeshTables : public mesh::MeshTables { } return NULL; } + void clearRecentRepeaters() { + memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); + _next_recent_repeater_idx = 0; + } void resetStats() { _direct_dups = _flood_dups = 0; } }; From c20e57385a6b9912663553040153732fe31b5703 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 7 May 2026 16:51:49 -0700 Subject: [PATCH 69/94] Fix direct retry diagnostics --- docs/cli_commands.md | 10 ++- examples/simple_repeater/MyMesh.cpp | 133 ++++++++++++++++++++-------- src/Mesh.cpp | 80 +++++++++++++---- src/Mesh.h | 1 + 4 files changed, 162 insertions(+), 62 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 78f8626d79..12f49de2d2 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -111,10 +111,12 @@ - `page`: 1-based page number **Notes:** -- `set` is rejected when the prefix already exists in neighbors. -- Rows are shown newest-first. -- Serial CLI prints all rows (no paging). -- Over LoRa remote CLI, page size is fixed at `4` rows; choose page with `get recent.repeater `. +- `set` stores or updates the prefix in the recent repeater table. +- Rows are sorted by prefix width (3-byte, 2-byte, 1-byte), then SNR descending. +- A full direct retry failure lowers the stored SNR by `0.25 dB`. +- If a full failure has no row yet, it first seeds the row at the active retry cutoff + `2.5 dB`, then applies the `0.25 dB` penalty. +- Serial CLI page size is fixed at `128` rows; choose page with `get recent.repeater `. +- Over LoRa remote CLI, page size is fixed at `7` rows; choose page with `get recent.repeater `. --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 10be221264..34b9bb577b 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1,5 +1,6 @@ #include "MyMesh.h" #include +#include /* ------------------------------ Config -------------------------------- */ @@ -124,6 +125,45 @@ static void formatRecentRepeaterSnrX4(int8_t snr_x4, char* out, size_t out_len) } } +static int buildSortedRecentRepeaterView(SimpleMeshTables* tables, + const SimpleMeshTables::RecentRepeaterInfo** out, + int out_cap) { + if (tables == NULL || out == NULL || out_cap <= 0) { + return 0; + } + + int total = tables->getRecentRepeaterCount(); + if (total > out_cap) { + total = out_cap; + } + + int count = 0; + for (int i = 0; i < total; i++) { + const auto* info = tables->getRecentRepeaterNewestByIdx(i); + if (info != NULL) { + out[count++] = info; + } + } + + std::stable_sort(out, out + count, [](const SimpleMeshTables::RecentRepeaterInfo* a, + const SimpleMeshTables::RecentRepeaterInfo* b) { + uint8_t a_len = a->prefix_len; + uint8_t b_len = b->prefix_len; + if (a_len > MAX_ROUTE_HASH_BYTES) a_len = MAX_ROUTE_HASH_BYTES; + if (b_len > MAX_ROUTE_HASH_BYTES) b_len = MAX_ROUTE_HASH_BYTES; + + if (a_len != b_len) { + return a_len > b_len; + } + if (a->snr_x4 != b->snr_x4) { + return a->snr_x4 > b->snr_x4; + } + return false; + }); + + return count; +} + static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) { uint8_t code = flags & 0x03; uint8_t size_pow2 = (uint8_t)(1U << code); // legacy TRACE interpretation @@ -605,6 +645,9 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u if (packet == NULL) { return; } + if (strcmp(event, "failure") == 0) { + return; + } uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; uint8_t prefix_len = 0; @@ -1561,15 +1604,58 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } } else { - int total = tables->getRecentRepeaterCount(); + const long page_size = sender_timestamp == 0 ? 128 : 7; + long page_num = 1; + const char* arg = sub; + + if (strncmp(arg, "page ", 5) == 0) { + arg += 5; + while (*arg == ' ') arg++; + } + + if (*arg != 0) { + char* end_ptr = NULL; + page_num = strtol(arg, &end_ptr, 10); + while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; + if (end_ptr == NULL || page_num <= 0 || (end_ptr != NULL && *end_ptr != 0)) { + strcpy(reply, "Err - usage: get recent.repeater [page]"); + return; + } + } + + size_t sorted_size = sizeof(SimpleMeshTables::RecentRepeaterInfo*) * MAX_RECENT_REPEATERS; + const SimpleMeshTables::RecentRepeaterInfo** sorted_recent = + (const SimpleMeshTables::RecentRepeaterInfo**)malloc(sorted_size); + if (sorted_recent == NULL) { + strcpy(reply, "Err - unable to allocate recent repeater view"); + return; + } + + int total = buildSortedRecentRepeaterView(tables, sorted_recent, MAX_RECENT_REPEATERS); if (total <= 0) { strcpy(reply, "> none"); } else { + int total_pages = (total + (int)page_size - 1) / (int)page_size; + if (page_num > total_pages) { + sprintf(reply, "> none (page=%ld/%d)", page_num, total_pages); + free(sorted_recent); + return; + } + + int offset = ((int)page_num - 1) * (int)page_size; + int limit = total - offset; + if (limit > (int)page_size) { + limit = (int)page_size; + } + if (sender_timestamp == 0) { - // Serial CLI: print all entries (no paging). - Serial.printf("Recent repeater table (newest first, total=%d):\n", total); - for (int i = 0; i < total; i++) { - const auto* info = tables->getRecentRepeaterNewestByIdx(i); + Serial.printf("Recent repeater table (3-byte,2-byte,1-byte; SNR desc, page=%ld/%d, n=%d/%d):\n", + page_num, + total_pages, + limit, + total); + for (int i = 0; i < limit; i++) { + const auto* info = sorted_recent[offset + i]; if (info == NULL) { continue; } @@ -1592,40 +1678,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply (uint32_t)info->fail_count, info->snr_locked ? ",l" : ""); } - sprintf(reply, "> n=%d/%d", total, total); + sprintf(reply, "> page=%ld/%d n=%d/%d", page_num, total_pages, limit, total); } else { - // Remote CLI: page by fixed size to fit packet-limited reply payload. - long page_num = 1; - const long page_size = 4; - const char* arg = sub; - - if (strncmp(arg, "page ", 5) == 0) { - arg += 5; - while (*arg == ' ') arg++; - } - - if (*arg != 0) { - char* end_ptr = NULL; - page_num = strtol(arg, &end_ptr, 10); - while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; - if (end_ptr == NULL || page_num <= 0 || (end_ptr != NULL && *end_ptr != 0)) { - strcpy(reply, "Err - usage: get recent.repeater [page]"); - return; - } - } - - int total_pages = (total + (int)page_size - 1) / (int)page_size; - if (page_num > total_pages) { - sprintf(reply, "> none (page=%ld/%d)", page_num, total_pages); - return; - } - - int offset = ((int)page_num - 1) * (int)page_size; - int limit = total - offset; - if (limit > (int)page_size) { - limit = (int)page_size; - } - int written = snprintf(reply, 160, "> page=%ld/%d n=%d/%d", page_num, total_pages, limit, total); bool truncated = false; if (written < 0) { @@ -1635,7 +1689,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply for (int i = 0; i < limit; i++) { int idx = offset + i; - const auto* info = tables->getRecentRepeaterNewestByIdx(idx); + const auto* info = sorted_recent[idx]; if (info == NULL) { continue; } @@ -1668,6 +1722,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } } + free(sorted_recent); } } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands diff --git a/src/Mesh.cpp b/src/Mesh.cpp index bc34c5eed2..a84a1fdaf2 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -16,6 +16,7 @@ void Mesh::begin() { _direct_retries[i].priority = 0; _direct_retries[i].progress_marker = 0; _direct_retries[i].expect_path_growth = false; + _direct_retries[i].waiting_final_echo = false; _direct_retries[i].queued = false; _direct_retries[i].active = false; } @@ -26,7 +27,25 @@ void Mesh::loop() { Dispatcher::loop(); for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { - if (!_direct_retries[i].active || !_direct_retries[i].queued || !millisHasNowPassed(_direct_retries[i].retry_at)) { + if (!_direct_retries[i].active) { + continue; + } + + if (_direct_retries[i].waiting_final_echo) { + if (!millisHasNowPassed(_direct_retries[i].retry_at)) { + continue; + } + + uint32_t elapsed_millis = _direct_retries[i].retry_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at); + onDirectRetryEvent("failed_all_tries", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + onDirectRetryEvent("failure", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + clearDirectRetrySlot(i); + continue; + } + + if (!_direct_retries[i].queued || !millisHasNowPassed(_direct_retries[i].retry_at)) { continue; } @@ -454,6 +473,9 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { } void Mesh::clearDirectRetrySlot(int idx) { + if (_direct_retries[idx].waiting_final_echo && _direct_retries[idx].packet != NULL) { + releasePacket(_direct_retries[idx].packet); + } _direct_retries[idx].packet = NULL; _direct_retries[idx].trigger_packet = NULL; _direct_retries[idx].retry_at = 0; @@ -462,6 +484,7 @@ void Mesh::clearDirectRetrySlot(int idx) { _direct_retries[idx].priority = 0; _direct_retries[idx].progress_marker = 0; _direct_retries[idx].expect_path_growth = false; + _direct_retries[idx].waiting_final_echo = false; _direct_retries[idx].queued = false; _direct_retries[idx].active = false; } @@ -497,24 +520,30 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { continue; } - if (_direct_retries[i].queued) { - if (_direct_retries[i].expect_path_growth - && _direct_retries[i].packet != NULL - && _direct_retries[i].progress_marker < packet->path_len) { - // For retry-good quality, use the received echo packet SNR (return-link quality). - _direct_retries[i].packet->_snr = packet->_snr; + int8_t echo_snr_x4 = packet->_snr; + if (_direct_retries[i].queued || _direct_retries[i].waiting_final_echo) { + if (_direct_retries[i].packet != NULL) { + // Success quality comes from the received downstream echo, not the original upstream RX. + _direct_retries[i].packet->_snr = echo_snr_x4; } - for (int j = 0; j < _mgr->getOutboundTotal(); j++) { - if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { - Packet* pending = _mgr->removeOutboundByIdx(j); - if (pending) { - releasePacket(pending); + uint32_t echo_millis = _direct_retries[i].echo_wait_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _direct_retries[i].echo_wait_started_at); + uint8_t retry_attempt = _direct_retries[i].waiting_final_echo + ? _direct_retries[i].retry_attempts_sent + : _direct_retries[i].retry_attempts_sent + 1; + onDirectRetryEvent("good", _direct_retries[i].packet, echo_millis, retry_attempt); + if (_direct_retries[i].queued) { + for (int j = 0; j < _mgr->getOutboundTotal(); j++) { + if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { + Packet* pending = _mgr->removeOutboundByIdx(j); + if (pending) { + releasePacket(pending); + } + break; } - break; } } - onDirectRetryEvent("canceled_echo", _direct_retries[i].packet, 0); - onDirectRetryEvent("good", _direct_retries[i].packet, 0); clearDirectRetrySlot(i); } else { if (_direct_retries[i].expect_path_growth @@ -551,9 +580,19 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX; } if (_direct_retries[i].retry_attempts_sent >= max_attempts) { - onDirectRetryEvent("failed_all_tries", packet, 0); - onDirectRetryEvent("failure", packet, 0); - clearDirectRetrySlot(i); + Packet* final_wait = obtainNewPacket(); + if (final_wait == NULL) { + onDirectRetryEvent("dropped_no_packet", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + clearDirectRetrySlot(i); + continue; + } + + *final_wait = *packet; + _direct_retries[i].packet = final_wait; + _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); + _direct_retries[i].waiting_final_echo = true; + _direct_retries[i].queued = false; continue; } @@ -572,7 +611,8 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { _direct_retries[i].packet = retry; _direct_retries[i].retry_delay = retry_delay; _direct_retries[i].retry_at = futureMillis(retry_delay); - onDirectRetryEvent("queued", retry, retry_delay); + _direct_retries[i].waiting_final_echo = false; + onDirectRetryEvent("queued", retry, retry_delay, _direct_retries[i].retry_attempts_sent + 1); } else { onDirectRetryEvent("dropped_queue_full", retry, retry_delay); onDirectRetryEvent("failure", retry, 0); @@ -603,6 +643,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { _direct_retries[i].packet = retry; _direct_retries[i].trigger_packet = NULL; _direct_retries[i].queued = true; + _direct_retries[i].waiting_final_echo = false; _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); onDirectRetryEvent("queued", retry, _direct_retries[i].retry_delay); } else { @@ -726,6 +767,7 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { _direct_retries[slot_idx].priority = priority; _direct_retries[slot_idx].progress_marker = progress_marker; _direct_retries[slot_idx].expect_path_growth = expect_path_growth; + _direct_retries[slot_idx].waiting_final_echo = false; _direct_retries[slot_idx].queued = false; _direct_retries[slot_idx].active = true; onDirectRetryEvent("armed", packet, retry_delay); diff --git a/src/Mesh.h b/src/Mesh.h index dd58d754aa..85848875b9 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -39,6 +39,7 @@ class Mesh : public Dispatcher { uint8_t priority; uint8_t progress_marker; bool expect_path_growth; + bool waiting_final_echo; bool queued; bool active; }; From dea5ed790fe2166f7248da97270f186c1f05e752 Mon Sep 17 00:00:00 2001 From: Nick Le Mouton Date: Fri, 5 Jun 2026 21:25:25 +1200 Subject: [PATCH 70/94] Add SECURITY.md --- SECURITY.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..a4b2207d55 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,57 @@ +# Security Policy + +## Supported Versions + +Security fixes are applied to the latest release only. We do not backport +fixes to older versions. + +| Version | Supported | +|---------|-----------| +| 1.15+ | ✅ | +| <1.15 | ❌ | + +## Reporting a Vulnerability + +**Please do not report security vulnerabilities through public GitHub issues.** + +Use GitHub's private vulnerability reporting instead: +1. Go to the **Security** tab of this repository +2. Click **Report a vulnerability** +3. Fill in the details and submit + +### What to include + +A useful report tells us: +- Which component or file is affected +- What an attacker can do (impact) and under what conditions +- A minimal reproduction case or proof-of-concept if you have one +- Whether you believe it is remotely exploitable + +You do not need a working exploit to report. An incomplete report is better +than no report. + +## What to expect + +This is a volunteer-maintained open-source project. We will do our best to +respond in a reasonable timeframe, but cannot commit to specific deadlines. + +We ask that you give us a fair opportunity to investigate and address the +issue before any public disclosure. If you have not heard back after +**90 days**, feel free to follow up or proceed with disclosure at your +discretion. + +## Scope + +In scope: +- Remote code execution, memory corruption, or denial-of-service via crafted + radio packets +- Authentication or encryption bypasses +- Vulnerabilities in the packet routing or path handling logic + +Out of scope: +- Physical access attacks (e.g., JTAG, UART extraction of keys) +- Regulatory compliance (duty cycle, frequency restrictions) +- Jamming or other physical-layer radio interference +- Issues in third-party libraries (RadioLib, Crypto, etc.) — report those + upstream +- "Best practice" suggestions without a demonstrated attack path From e67933ca2a39e0cb590d53c4c310d6380ee0f115 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Mon, 8 Jun 2026 21:11:49 +0700 Subject: [PATCH 71/94] PowerSaving 16 --- boards/nrf52840_s140_v6.ld | 15 +- boards/nrf52840_s140_v6_extrafs.ld | 15 +- boards/nrf52840_s140_v7.ld | 15 +- boards/nrf52840_s140_v7_extrafs.ld | 12 +- build-iotthinks.sh | 135 ++++++++++++++++ build.sh | 7 +- docs/cli_commands.md | 61 +++++++ examples/companion_radio/DataStore.cpp | 2 + examples/companion_radio/MyMesh.cpp | 29 ++++ examples/companion_radio/NodePrefs.h | 1 + examples/companion_radio/main.cpp | 40 +++++ examples/simple_repeater/MyMesh.cpp | 153 ++++++++++++++++-- examples/simple_repeater/MyMesh.h | 2 +- examples/simple_repeater/main.cpp | 7 +- examples/simple_room_server/MyMesh.cpp | 10 ++ examples/simple_room_server/MyMesh.h | 3 + examples/simple_room_server/main.cpp | 13 ++ src/MeshCore.h | 3 + src/helpers/ArduinoHelpers.cpp | 6 + src/helpers/ArduinoHelpers.h | 42 ++++- src/helpers/ArduinoSerialInterface.cpp | 4 + src/helpers/ArduinoSerialInterface.h | 1 + src/helpers/BaseSerialInterface.h | 1 + src/helpers/ClientACL.h | 3 +- src/helpers/CommonCLI.cpp | 92 ++++++++++- src/helpers/CommonCLI.h | 2 + src/helpers/ESP32Board.h | 93 ++++++++++- src/helpers/MeshadventurerBoard.h | 30 ---- src/helpers/esp32/SerialBLEInterface.cpp | 4 + src/helpers/esp32/SerialBLEInterface.h | 1 + src/helpers/esp32/SerialWifiInterface.cpp | 4 + src/helpers/esp32/SerialWifiInterface.h | 1 + src/helpers/esp32/TBeamBoard.h | 24 --- src/helpers/nrf52/SerialBLEInterface.cpp | 4 + src/helpers/nrf52/SerialBLEInterface.h | 1 + .../sensors/EnvironmentSensorManager.cpp | 15 ++ .../sensors/MicroNMEALocationProvider.h | 2 + src/helpers/ui/ST7735Display.cpp | 9 ++ variants/gat562_30s_mesh_kit/platformio.ini | 3 +- variants/gat562_mesh_evb_pro/platformio.ini | 2 +- .../gat562_mesh_tracker_pro/platformio.ini | 3 +- variants/gat562_mesh_watch13/platformio.ini | 2 +- variants/heltec_ct62/platformio.ini | 4 + variants/heltec_e213/HeltecE213Board.cpp | 27 ---- variants/heltec_e213/HeltecE213Board.h | 3 - variants/heltec_e290/HeltecE290Board.cpp | 27 ---- variants/heltec_e290/HeltecE290Board.h | 3 - variants/heltec_t096/LoRaFEMControl.h | 5 +- variants/heltec_t096/T096Board.cpp | 24 ++- variants/heltec_t096/T096Board.h | 3 + variants/heltec_t096/platformio.ini | 8 +- variants/heltec_t114/platformio.ini | 2 +- variants/heltec_t190/HeltecT190Board.cpp | 27 ---- variants/heltec_t190/HeltecT190Board.h | 3 - variants/heltec_tracker/platformio.ini | 4 + .../HeltecTrackerV2Board.cpp | 49 +++--- .../heltec_tracker_v2/HeltecTrackerV2Board.h | 5 +- variants/heltec_tracker_v2/LoRaFEMControl.h | 5 +- variants/heltec_tracker_v2/platformio.ini | 4 + variants/heltec_v2/HeltecV2Board.h | 25 --- variants/heltec_v2/platformio.ini | 4 + variants/heltec_v3/HeltecV3Board.h | 29 ---- variants/heltec_v3/platformio.ini | 8 + variants/heltec_v4/HeltecV4Board.cpp | 53 +++--- variants/heltec_v4/HeltecV4Board.h | 5 +- variants/heltec_v4/LoRaFEMControl.h | 5 +- variants/heltec_v4/platformio.ini | 18 +++ variants/heltec_wireless_paper/platformio.ini | 4 + variants/lilygo_t3s3/platformio.ini | 4 + variants/lilygo_tbeam_1w/platformio.ini | 5 + variants/lilygo_tbeam_SX1262/platformio.ini | 7 + variants/lilygo_tbeam_SX1276/platformio.ini | 4 + .../platformio.ini | 4 + variants/lilygo_tdeck/TDeckBoard.h | 24 --- variants/lilygo_tlora_v2_1/platformio.ini | 6 +- variants/promicro/platformio.ini | 1 + variants/rak3112/RAK3112Board.h | 29 ---- variants/rak3401/platformio.ini | 3 +- variants/rak4631/platformio.ini | 3 +- variants/rak_wismesh_tag/platformio.ini | 1 + variants/sensecap_solar/platformio.ini | 2 +- variants/station_g2/StationG2Board.h | 24 --- variants/thinknode_m2/ThinknodeM2Board.cpp | 52 +++--- variants/thinknode_m2/ThinknodeM2Board.h | 4 - variants/thinknode_m5/ThinknodeM5Board.cpp | 8 - variants/thinknode_m5/ThinknodeM5Board.h | 3 - variants/xiao_c3/XiaoC3Board.h | 32 ---- variants/xiao_c3/platformio.ini | 5 + variants/xiao_c6/platformio.ini | 1 + variants/xiao_nrf52/platformio.ini | 2 +- variants/xiao_s3/platformio.ini | 4 + variants/xiao_s3_wio/platformio.ini | 4 + 92 files changed, 997 insertions(+), 461 deletions(-) create mode 100644 build-iotthinks.sh create mode 100644 src/helpers/ArduinoHelpers.cpp diff --git a/boards/nrf52840_s140_v6.ld b/boards/nrf52840_s140_v6.ld index 6dad975b0d..d0c7d1dc8b 100644 --- a/boards/nrf52840_s140_v6.ld +++ b/boards/nrf52840_s140_v6.ld @@ -7,6 +7,9 @@ MEMORY { FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xED000 - 0x26000 + /* To keep data in RAM across resets */ + PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8 + /* SRAM required by Softdevice depend on * - Attribute Table Size (Number of Services and Characteristics) * - Vendor UUID count @@ -14,11 +17,19 @@ MEMORY * - Concurrent connection peripheral + central + secure links * - Event Len, HVN queue, Write CMD queue */ - RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 + RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8 } SECTIONS { + . = ALIGN(4); + .persistent (NOLOAD) : + { + KEEP(*(.persistent_magic)) + KEEP(*(.persistent_data)) + . = ALIGN(4); + } > PERSISTENT_RAM + . = ALIGN(4); .svc_data : { @@ -33,6 +44,6 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM -} INSERT AFTER .data; +} INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v6_extrafs.ld b/boards/nrf52840_s140_v6_extrafs.ld index 352610679e..bd4547473a 100644 --- a/boards/nrf52840_s140_v6_extrafs.ld +++ b/boards/nrf52840_s140_v6_extrafs.ld @@ -7,6 +7,9 @@ MEMORY { FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xD4000 - 0x26000 + /* To keep data in RAM across resets */ + PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8 + /* SRAM required by Softdevice depend on * - Attribute Table Size (Number of Services and Characteristics) * - Vendor UUID count @@ -14,11 +17,19 @@ MEMORY * - Concurrent connection peripheral + central + secure links * - Event Len, HVN queue, Write CMD queue */ - RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 + RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8 } SECTIONS { + . = ALIGN(4); + .persistent (NOLOAD) : + { + KEEP(*(.persistent_magic)) + KEEP(*(.persistent_data)) + . = ALIGN(4); + } > PERSISTENT_RAM + . = ALIGN(4); .svc_data : { @@ -33,6 +44,6 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM -} INSERT AFTER .data; +} INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v7.ld b/boards/nrf52840_s140_v7.ld index 6aaeb4034f..2333238f28 100644 --- a/boards/nrf52840_s140_v7.ld +++ b/boards/nrf52840_s140_v7.ld @@ -7,6 +7,9 @@ MEMORY { FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xED000 - 0x27000 + /* To keep data in RAM across resets */ + PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8 + /* SRAM required by Softdevice depend on * - Attribute Table Size (Number of Services and Characteristics) * - Vendor UUID count @@ -14,11 +17,19 @@ MEMORY * - Concurrent connection peripheral + central + secure links * - Event Len, HVN queue, Write CMD queue */ - RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 + RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8 } SECTIONS { + . = ALIGN(4); + .persistent (NOLOAD) : + { + KEEP(*(.persistent_magic)) + KEEP(*(.persistent_data)) + . = ALIGN(4); + } > PERSISTENT_RAM + . = ALIGN(4); .svc_data : { @@ -33,6 +44,6 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM -} INSERT AFTER .data; +} INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v7_extrafs.ld b/boards/nrf52840_s140_v7_extrafs.ld index 5956183aa3..48348188af 100644 --- a/boards/nrf52840_s140_v7_extrafs.ld +++ b/boards/nrf52840_s140_v7_extrafs.ld @@ -14,11 +14,19 @@ MEMORY * - Concurrent connection peripheral + central + secure links * - Event Len, HVN queue, Write CMD queue */ - RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 + RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8 } SECTIONS { + . = ALIGN(4); + .persistent (NOLOAD) : + { + KEEP(*(.persistent_magic)) + KEEP(*(.persistent_data)) + . = ALIGN(4); + } > PERSISTENT_RAM + . = ALIGN(4); .svc_data : { @@ -33,6 +41,6 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM -} INSERT AFTER .data; +} INCLUDE "nrf52_common.ld" diff --git a/build-iotthinks.sh b/build-iotthinks.sh new file mode 100644 index 0000000000..613789d08a --- /dev/null +++ b/build-iotthinks.sh @@ -0,0 +1,135 @@ +# sh ./build-repeaters-iotthinks.sh +export FIRMWARE_VERSION="PowerSaving16" + +############# Repeaters ############# +# Commonly-used boards +## ESP32 - 17 boards +sh build.sh build-firmware \ +Heltec_v3_repeater \ +Heltec_WSL3_repeater \ +heltec_v4_repeater \ +Station_G2_repeater \ +T_Beam_S3_Supreme_SX1262_repeater \ +Tbeam_SX1262_repeater \ +LilyGo_T3S3_sx1262_repeater \ +Xiao_S3_WIO_repeater \ +Xiao_C3_repeater \ +Xiao_C6_repeater_ \ +Heltec_E290_repeater \ +Heltec_Wireless_Tracker_repeater \ +LilyGo_TBeam_1W_repeater \ +Xiao_S3_repeater \ +heltec_tracker_v2_repeater \ +Heltec_Wireless_Paper_repeater \ +Heltec_ct62_repeater + +## NRF52 - 17 boards +sh build.sh build-firmware \ +RAK_4631_repeater \ +Heltec_t114_repeater \ +Xiao_nrf52_repeater \ +Heltec_mesh_solar_repeater \ +ProMicro_repeater \ +SenseCap_Solar_repeater \ +t1000e_repeater \ +LilyGo_T-Echo_repeater \ +WioTrackerL1_repeater \ +RAK_3401_repeater \ +RAK_WisMesh_Tag_repeater \ +GAT562_30S_Mesh_Kit_repeater \ +GAT562_Mesh_Tracker_Pro_repeater \ +ikoka_nano_nrf_22dbm_repeater \ +ikoka_nano_nrf_30dbm_repeater \ +ikoka_nano_nrf_33dbm_repeater \ +ThinkNode_M1_repeater \ +Heltec_t096_repeater + +## ESP32, SX1276 - 3 boards +sh build.sh build-firmware \ +Heltec_v2_repeater \ +LilyGo_TLora_V2_1_1_6_repeater \ +Tbeam_SX1276_repeater + +############# Room Server ############# +# ESP32 - 7 boards +sh build.sh build-firmware \ +Heltec_v3_room_server \ +heltec_v4_room_server \ +LilyGo_TBeam_1W_room_server \ +Heltec_WSL3_room_server \ +Xiao_S3_room_server \ +heltec_tracker_v2_room_server \ +Heltec_Wireless_Paper_room_server + +# NRF52 - 6 boards +sh build.sh build-firmware \ +RAK_4631_room_server \ +Heltec_t114_room_server \ +Xiao_nrf52_room_server \ +t1000e_room_server \ +WioTrackerL1_room_server \ +RAK_3401_room_server \ +Heltec_t096_room_server + +############# Companions BLE ############# +# NRF52 - 12 boards +sh build.sh build-firmware \ +RAK_4631_companion_radio_ble \ +Heltec_t114_companion_radio_ble \ +Xiao_nrf52_companion_radio_ble \ +t1000e_companion_radio_ble \ +LilyGo_T-Echo_companion_radio_ble \ +WioTrackerL1_companion_radio_ble \ +RAK_3401_companion_radio_ble \ +RAK_WisMesh_Tag_companion_radio_ble \ +SenseCap_Solar_companion_radio_ble \ +ThinkNode_M1_companion_radio_ble \ +Heltec_t096_companion_radio_ble \ +Heltec_t096_companion_radio_ble_femoff + +############# Companions BLE PS ############# +# ESP32 - 18 boards +sh build.sh build-firmware \ +Heltec_v3_companion_radio_ble_ps \ +heltec_v4_companion_radio_ble_ps \ +heltec_v4_3_companion_radio_ble_ps_femoff \ +Xiao_C3_companion_radio_ble_ps \ +Xiao_S3_companion_radio_ble_ps \ +Xiao_S3_WIO_companion_radio_ble_ps \ +Heltec_v2_companion_radio_ble_ps \ +LilyGo_TBeam_1W_companion_radio_ble_ps \ +Heltec_WSL3_companion_radio_ble_ps \ +Heltec_Wireless_Tracker_companion_radio_ble_ps \ +heltec_tracker_v2_companion_radio_ble_ps \ +Heltec_Wireless_Paper_companion_radio_ble_ps \ +LilyGo_TLora_V2_1_1_6_companion_radio_ble_ps \ +Heltec_ct62_companion_radio_ble_ps \ +T_Beam_S3_Supreme_SX1262_companion_radio_ble_ps \ +Tbeam_SX1262_companion_radio_ble_ps \ +heltec_v4_expansionkit_tft_companion_radio_ble_ps \ +LilyGo_T3S3_sx1262_companion_radio_ble_ps + +# Not working +Tbeam_SX1276_companion_radio_ble_ps \ + +############# Companions USB ############# +sh build.sh build-firmware \ +Heltec_t096_companion_radio_usb + +############# Sample builds ############# +# 14 boards +sh build.sh build-firmware \ +Heltec_v3_repeater \ +heltec_v4_repeater \ +Xiao_C3_repeater \ +Xiao_C6_repeater_ \ +RAK_4631_repeater \ +Heltec_t096_repeater \ +Heltec_v3_companion_radio_ble_ps \ +heltec_v4_companion_radio_ble_ps \ +heltec_v4_3_companion_radio_ble_ps_femoff \ +Xiao_C3_companion_radio_ble_ps \ +Xiao_C6_companion_radio_ble_ \ +RAK_4631_companion_radio_ble \ +Heltec_t096_companion_radio_ble \ +Heltec_t096_companion_radio_ble_femoff diff --git a/build.sh b/build.sh index 313c4c47a0..006eae9698 100755 --- a/build.sh +++ b/build.sh @@ -134,7 +134,8 @@ build_firmware() { # set firmware version string # e.g: v1.0.0-abcdef - FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}-${COMMIT_HASH}" + # FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}-${COMMIT_HASH}" + FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}" # craft filename # e.g: RAK_4631_Repeater-v1.0.0-SHA @@ -152,8 +153,8 @@ build_firmware() { # build merge-bin for esp32 fresh install, copy .bins to out folder (e.g: Heltec_v3_room_server-v1.0.0-SHA.bin) if [ "$ENV_PLATFORM" == "ESP32_PLATFORM" ]; then pio run -t mergebin -e $1 - cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true - cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-merged.bin 2>/dev/null || true + cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}-upgrade.bin 2>/dev/null || true + cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-freshInstall-merged.bin 2>/dev/null || true fi # build .uf2 for nrf52 boards, copy .uf2 and .zip to out folder (e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 9accb2998d..4d3270b27b 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -219,6 +219,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change the boosted receive gain mode +**Usage:** +- `get radio.rxgain` +- `set radio.rxgain ` + +**Parameters:** +- `state`: `on`|`off` + +**Default:** `off` + +**Note:** Only available on SX1262 and SX1268 based boards. + +--- + #### Change the radio parameters for a set duration **Usage:** - `tempradio ,,,,` @@ -263,6 +277,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change the LoRa FEM receive-path gain state on supported boards +**Usage:** +- `get radio.fem.rxgain` +- `set radio.fem.rxgain ` + +**Parameters:** +- `state`: `on`|`off` + +**Notes:** +- This controls the external LoRa FEM receive-path LNA where the board supports it. +- This is separate from `radio.rxgain`, which controls the radio chip receive gain mode. + +--- + ### System #### View or change this node's name @@ -417,6 +445,18 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or set reboot interval (Repeater and room server) +**Usage:** +- `get reboot.interval` +- `set reboot.interval ` + +**Parameters:** +- `hours`: 0-255. 0 is disabled + +**Default:** `0` (disabled) + +--- + ### Routing #### View or change this node's repeat flag @@ -752,6 +792,27 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or set the direct path override for the current remote client +**Usage:** +- `get outpath` +- `set outpath ` +- `set outpath direct` +- `set outpath clear` +- `set outpath flood` + +**Parameters:** +- `hopN_hex`: Hop hash, `2`, `4`, or `6` hex characters. All hops must use the same width. + +**Notes:** +- These commands require remote client context (they target the caller's ACL entry). +- The path hash size is inferred from the hop hash width. +- `outpath` overrides the primary direct route used for replies to the caller. +- `direct` sets a zero-hop direct route for a caller reachable without repeaters. +- `clear` forgets the current direct path and allows normal path discovery to repopulate it. +- `flood` forces replies to use flood packets until the client logs in again. + +--- + #### Create a new region **Usage:** - `region put [parent_name]` diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index bf2f36c3d9..fdb924ad4c 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -233,6 +233,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 + file.read((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122 file.close(); } @@ -273,6 +274,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 + file.write((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122 file.close(); } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 6fbb0f7428..6bf2671a3e 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -62,6 +62,8 @@ #define CMD_SET_DEFAULT_FLOOD_SCOPE 63 #define CMD_GET_DEFAULT_FLOOD_SCOPE 64 #define CMD_SEND_RAW_PACKET 65 +#define CMD_GET_RADIO_FEM_RXGAIN 66 +#define CMD_SET_RADIO_FEM_RXGAIN 67 // Stats sub-types for CMD_GET_STATS #define STATS_TYPE_CORE 0 @@ -886,6 +888,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.rx_boosted_gain = 1; // enabled by default #endif #endif + _prefs.radio_fem_rxgain = 1; } void MyMesh::begin(bool has_display) { @@ -935,6 +938,7 @@ void MyMesh::begin(bool has_display) { _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER); _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1 _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours + _prefs.radio_fem_rxgain = constrain(_prefs.radio_fem_rxgain, 0, 1); #ifdef BLE_PIN_CODE // 123456 by default if (_prefs.ble_pin == 0) { @@ -964,6 +968,7 @@ void MyMesh::begin(bool has_display) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); + board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); } @@ -1821,6 +1826,30 @@ void MyMesh::handleCmdFrame(size_t len) { } else { writeErrFrame(ERR_CODE_ILLEGAL_ARG); } + } else if (cmd_frame[0] == CMD_GET_RADIO_FEM_RXGAIN) { + if (!board.canControlLoRaFemLna()) { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + } else { + out_frame[0] = RESP_CODE_OK; + uint8_t value = board.isLoRaFemLnaEnabled() ? 1 : 0; + memcpy(&out_frame[1], &value, 1); + _serial->writeFrame(out_frame, 2); + } + } else if (cmd_frame[0] == CMD_SET_RADIO_FEM_RXGAIN && len >= 2) { + uint8_t value = cmd_frame[1]; + if (!board.canControlLoRaFemLna()) { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + } else if (value <= 1) { + _prefs.radio_fem_rxgain = value; + if (board.setLoRaFemLnaEnabled(value != 0)) { + savePrefs(); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + } + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } } else if (cmd_frame[0] == CMD_GET_ADVERT_PATH && len >= PUB_KEY_SIZE+2) { // FUTURE use: uint8_t reserved = cmd_frame[1]; uint8_t *pub_key = &cmd_frame[2]; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 48c381ceaf..6598a69c60 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -29,6 +29,7 @@ struct NodePrefs { // persisted to file uint32_t gps_interval; // GPS read interval in seconds 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 uint8_t client_repeat; uint8_t path_hash_mode; // which path mode to use when sending uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index ef9b6bfca4..f10cb17029 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -2,6 +2,11 @@ #include #include "MyMesh.h" +#ifdef ESP32_PLATFORM +#include "esp_pm.h" +#include "esp_bt.h" +#endif + // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -240,6 +245,37 @@ void setup() { #endif board.onBootComplete(); + +#ifdef ESP32_PLATFORM +#if !CONFIG_IDF_TARGET_ESP32C6 + // Enable BLE sleep + esp_err_t errBLESleep = esp_bt_sleep_enable(); + if (errBLESleep == ESP_OK) { + Serial.println("Bluetooth sleep enabled successfully"); + } else { + Serial.printf("Bluetooth sleep enable failed: %s\n", esp_err_to_name(errBLESleep)); + } +#endif + +#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; +#elif CONFIG_IDF_TARGET_ESP32C6 + esp_pm_config_t pm_config; +#endif + + // Configure Power Management + pm_config = { .max_freq_mhz = 80, .min_freq_mhz = 40, .light_sleep_enable = true }; + esp_err_t errPM = esp_pm_configure(&pm_config); + if (errPM == ESP_OK) { + Serial.println("Power Management configured successfully"); + } else { + Serial.printf("Power Management failed to configure: %d\r\n", errPM); + } +#endif } void loop() { @@ -253,6 +289,10 @@ void loop() { if (!the_mesh.hasPendingWork()) { #if defined(NRF52_PLATFORM) board.sleep(0); // nrf ignores seconds param, sleeps whenever possible +#elif defined(ESP32_PLATFORM) + if (!serial_interface.isReadBusy() && !serial_interface.isWriteBusy()) { // BLE is not busy + vTaskDelay(pdMS_TO_TICKS(10)); // attempt to sleep + } #endif } diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 096907494b..1b0ca1916c 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -676,7 +676,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); if (reply) { - if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT + if (mesh::Packet::isValidPathLen(client->out_path_len)) { // we have an out_path, so send DIRECT sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); } else { sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); @@ -709,10 +709,10 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, mesh::Packet *ack = createAck(ack_hash); if (ack) { - if (client->out_path_len == OUT_PATH_UNKNOWN) { - sendFloodReply(ack, TXT_ACK_DELAY, packet->getPathHashSize()); - } else { + if (mesh::Packet::isValidPathLen(client->out_path_len)) { sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY); + } else { + sendFloodReply(ack, TXT_ACK_DELAY, packet->getPathHashSize()); } } } @@ -723,7 +723,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, if (is_retry) { *reply = 0; } else { - handleCommand(sender_timestamp, command, reply); + handleCommand(sender_timestamp, client, command, reply); } int text_len = strlen(reply); if (text_len > 0) { @@ -737,10 +737,10 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); if (reply) { - if (client->out_path_len == OUT_PATH_UNKNOWN) { - sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); - } else { + if (mesh::Packet::isValidPathLen(client->out_path_len)) { sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS); + } else { + sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); } } } @@ -760,7 +760,9 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t auto client = acl.getClientByIdx(i); // store a copy of path, for sendDirect() - client->out_path_len = mesh::Packet::copyPath(client->out_path, path, path_len); + if (client->out_path_len != OUT_PATH_FORCE_FLOOD) { + client->out_path_len = mesh::Packet::copyPath(client->out_path, path, path_len); + } client->last_activity = getRTCClock()->getCurrentTime(); } else { MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i); @@ -917,6 +919,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.rx_boosted_gain = 1; // enabled by default; #endif #endif + _prefs.radio_fem_rxgain = 1; pending_discover_tag = 0; pending_discover_until = 0; @@ -965,6 +968,7 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); + board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); updateAdvertTimer(); updateFloodAdvertTimer(); @@ -1171,7 +1175,108 @@ void MyMesh::clearStats() { ((SimpleMeshTables *)getTables())->resetStats(); } -void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) { +static char* trimSpaces(char* s) { + while (*s == ' ') s++; + char* end = s + strlen(s); + while (end > s && end[-1] == ' ') end--; + *end = 0; + return s; +} + +static bool parsePathCommand(char* raw, uint8_t* out_path, uint8_t& out_path_len, const char*& err) { + if (raw == NULL || out_path == NULL) { + err = "Err - bad params"; + return false; + } + + char* spec = trimSpaces(raw); + if (*spec == 0) { + err = "Err - missing path"; + return false; + } + if (strcmp(spec, "clear") == 0 || strcmp(spec, "-") == 0 || strcmp(spec, "none") == 0) { + out_path_len = OUT_PATH_UNKNOWN; + return true; + } + if (strcmp(spec, "flood") == 0) { + out_path_len = OUT_PATH_FORCE_FLOOD; + return true; + } + if (strcmp(spec, "direct") == 0) { + out_path_len = 0; + return true; + } + + uint8_t hash_size = 0; + uint8_t hop_count = 0; + char* token = spec; + while (token && *token) { + char* comma = strchr(token, ','); + if (comma) *comma = 0; + token = trimSpaces(token); + + int hex_len = strlen(token); + if (!(hex_len == 2 || hex_len == 4 || hex_len == 6)) { + err = "Err - bad params"; + return false; + } + + uint8_t hop_hash_size = (uint8_t)(hex_len / 2); + if (hash_size == 0) { + hash_size = hop_hash_size; + } else if (hash_size != hop_hash_size) { + err = "Err - bad params"; + return false; + } + + if (hop_count >= 63 || (hop_count + 1) * hash_size > MAX_PATH_SIZE) { + err = "Err - bad params"; + return false; + } + if (!mesh::Utils::fromHex(&out_path[hop_count * hash_size], hash_size, token)) { + err = "Err - bad hex"; + return false; + } + + hop_count++; + token = comma ? comma + 1 : NULL; + } + + if (hash_size == 0 || hop_count == 0) { + err = "Err - missing path"; + return false; + } + out_path_len = ((hash_size - 1) << 6) | (hop_count & 63); + return true; +} + +static void formatPathReply(const uint8_t* path, uint8_t path_len, char* out, size_t out_len) { + if (path_len == OUT_PATH_FORCE_FLOOD) { + snprintf(out, out_len, "> flood"); + return; + } + if (path_len == OUT_PATH_UNKNOWN) { + snprintf(out, out_len, "> unknown"); + return; + } + if (!mesh::Packet::isValidPathLen(path_len)) { + snprintf(out, out_len, "> invalid"); + return; + } + if ((path_len & 63) == 0) { + snprintf(out, out_len, "> direct"); + return; + } + + uint8_t hash_size = (path_len >> 6) + 1; + uint8_t hop_count = path_len & 63; + uint8_t byte_len = hop_count * hash_size; + char hex[(MAX_PATH_SIZE * 2) + 1]; + mesh::Utils::toHex(hex, path, byte_len); + snprintf(out, out_len, "> hs=%u hops=%u hex=%s", (uint32_t)hash_size, (uint32_t)hop_count, hex); +} + +void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *command, char *reply) { if (region_load_active) { if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation region_map = temp_map; // copy over the temp instance as new current map @@ -1248,6 +1353,34 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply Serial.printf("\n"); } reply[0] = 0; + } else if (strcmp(command, "get outpath") == 0 + || strcmp(command, "set outpath") == 0 + || strncmp(command, "set outpath ", 12) == 0) { + bool is_get = strncmp(command, "get ", 4) == 0; + if (sender == NULL) { + strcpy(reply, "Err - command needs remote client context"); + } else if (is_get) { + formatPathReply(sender->out_path, sender->out_path_len, reply, 160); + } else { + char* spec = command + 11; // length of "set outpath" + if (*spec == ' ') spec++; + + uint8_t path[MAX_PATH_SIZE]; + uint8_t path_len = OUT_PATH_UNKNOWN; + const char* err = NULL; + if (!parsePathCommand(spec, path, path_len, err)) { + strcpy(reply, err ? err : "Err - invalid path"); + } else { + if (path_len == OUT_PATH_UNKNOWN || path_len == OUT_PATH_FORCE_FLOOD) { + memset(sender->out_path, 0, sizeof(sender->out_path)); + sender->out_path_len = path_len; + } else { + sender->out_path_len = mesh::Packet::copyPath(sender->out_path, path, path_len); + } + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + formatPathReply(sender->out_path, sender->out_path_len, reply, 160); + } + } } else if (memcmp(command, "discover.neighbors", 18) == 0) { const char* sub = command + 18; while (*sub == ' ') sub++; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 7597c6c6f6..fbc756f471 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -223,7 +223,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void saveIdentity(const mesh::LocalIdentity& new_id) override; void clearStats() override; - void handleCommand(uint32_t sender_timestamp, char* command, char* reply); + void handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char* command, char* reply); void loop(); #if defined(WITH_BRIDGE) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 2ce056f521..82e2a21215 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -122,7 +122,7 @@ void loop() { Serial.print('\n'); command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; - the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! + the_mesh.handleCommand(0, NULL, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } @@ -161,4 +161,9 @@ void loop() { } #endif } + + if (the_mesh.getNodePrefs()->reboot_interval > 0 && + the_mesh.millisHasNowPassed(the_mesh.getNodePrefs()->reboot_interval * 3600000)) { + board.reboot(); + } } diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 98b22fdb72..bbea97f5cf 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -658,6 +658,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.gps_enabled = 0; _prefs.gps_interval = 0; _prefs.advert_loc_policy = ADVERT_LOC_PREFS; + _prefs.radio_fem_rxgain = 1; next_post_idx = 0; next_client_idx = 0; @@ -699,6 +700,7 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); + board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); updateAdvertTimer(); updateFloodAdvertTimer(); @@ -1028,3 +1030,11 @@ void MyMesh::loop() { uptime_millis += now - last_millis; last_millis = now; } + +// To check if there is pending work +bool MyMesh::hasPendingWork() const { +#if defined(WITH_BRIDGE) + if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep +#endif + return _mgr->getOutboundTotal() > 0; +} diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 5277ddad61..24c26418e4 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -222,4 +222,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void clearStats() override; void handleCommand(uint32_t sender_timestamp, char* command, char* reply); void loop(); + + // To check if there is pending work + bool hasPendingWork() const; }; diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index a3798b2175..ad8aa9149d 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -18,6 +18,9 @@ void halt() { static char command[MAX_POST_TEXT_LEN+1]; +// For power saving +unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot + void setup() { Serial.begin(115200); delay(1000); @@ -115,4 +118,14 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + + if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { +#if defined(NRF52_PLATFORM) + board.sleep(0); // nrf ignores seconds param, sleeps whenever possible +#else + if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep + board.sleep(30); // Sleep. Wake up after a while or when receiving a LoRa packet + } +#endif + } } diff --git a/src/MeshCore.h b/src/MeshCore.h index cfa33cf90b..89e60b1f7e 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -64,6 +64,9 @@ class MainBoard { virtual uint8_t getStartupReason() const = 0; virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; } virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported + virtual bool setLoRaFemLnaEnabled(bool enable) { return false; } + virtual bool canControlLoRaFemLna() const { return false; } + virtual bool isLoRaFemLnaEnabled() const { return false; } // Power management interface (boards with power management override these) virtual bool isExternalPowered() { return false; } diff --git a/src/helpers/ArduinoHelpers.cpp b/src/helpers/ArduinoHelpers.cpp new file mode 100644 index 0000000000..feb77a79c6 --- /dev/null +++ b/src/helpers/ArduinoHelpers.cpp @@ -0,0 +1,6 @@ +#include + +extern "C" { + __attribute__((section(".persistent_magic"))) uint32_t persistent_magic; + __attribute__((section(".persistent_data"))) uint32_t persistent_time; +} \ No newline at end of file diff --git a/src/helpers/ArduinoHelpers.h b/src/helpers/ArduinoHelpers.h index 97596daa31..9b50b98caf 100644 --- a/src/helpers/ArduinoHelpers.h +++ b/src/helpers/ArduinoHelpers.h @@ -3,19 +3,57 @@ #include #include +#ifdef NRF52_PLATFORM +#define CLOCK_MAGIC_NUM 0xAA55CC33 +#define RTC_TIME_MIN 1772323200 // 1 Mar 2026 + +extern uint32_t persistent_magic; +extern uint32_t persistent_time; +#endif + class VolatileRTCClock : public mesh::RTCClock { uint32_t base_time; uint64_t accumulator; unsigned long prev_millis; + public: - VolatileRTCClock() { base_time = 1715770351; accumulator = 0; prev_millis = millis(); } // 15 May 2024, 8:50pm + VolatileRTCClock() { +#ifdef NRF52_PLATFORM + if (persistent_magic == CLOCK_MAGIC_NUM && persistent_time >= RTC_TIME_MIN) { + base_time = persistent_time; + } else { + base_time = RTC_TIME_MIN; + } +#else + base_time = 1715770351; +#endif + + accumulator = 0; + prev_millis = millis(); + } + uint32_t getCurrentTime() override { return base_time + accumulator/1000; } - void setCurrentTime(uint32_t time) override { base_time = time; accumulator = 0; prev_millis = millis(); } + + void setCurrentTime(uint32_t time) override { + base_time = time; + accumulator = 0; + prev_millis = millis(); + +#ifdef NRF52_PLATFORM + persistent_magic = CLOCK_MAGIC_NUM; + persistent_time = time; +#endif + } void tick() override { unsigned long now = millis(); accumulator += (now - prev_millis); prev_millis = now; + +#ifdef NRF52_PLATFORM + persistent_magic = CLOCK_MAGIC_NUM; + persistent_time = getCurrentTime(); +#endif } }; diff --git a/src/helpers/ArduinoSerialInterface.cpp b/src/helpers/ArduinoSerialInterface.cpp index a01fa5866f..6b44397406 100644 --- a/src/helpers/ArduinoSerialInterface.cpp +++ b/src/helpers/ArduinoSerialInterface.cpp @@ -17,6 +17,10 @@ bool ArduinoSerialInterface::isConnected() const { return true; // no way of knowing, so assume yes } +bool ArduinoSerialInterface::isReadBusy() const { + return false; +} + bool ArduinoSerialInterface::isWriteBusy() const { return false; } diff --git a/src/helpers/ArduinoSerialInterface.h b/src/helpers/ArduinoSerialInterface.h index c4086353aa..3eed767134 100644 --- a/src/helpers/ArduinoSerialInterface.h +++ b/src/helpers/ArduinoSerialInterface.h @@ -28,6 +28,7 @@ class ArduinoSerialInterface : public BaseSerialInterface { bool isConnected() const override; + bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; diff --git a/src/helpers/BaseSerialInterface.h b/src/helpers/BaseSerialInterface.h index e9a3f2ab46..8ff110eb57 100644 --- a/src/helpers/BaseSerialInterface.h +++ b/src/helpers/BaseSerialInterface.h @@ -15,6 +15,7 @@ class BaseSerialInterface { virtual bool isConnected() const = 0; + virtual bool isReadBusy() const = 0; virtual bool isWriteBusy() const = 0; virtual size_t writeFrame(const uint8_t src[], size_t len) = 0; virtual size_t checkRecvFrame(uint8_t dest[]) = 0; diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index b758f7068d..e0b8c54287 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -10,7 +10,8 @@ #define PERM_ACL_READ_WRITE 2 #define PERM_ACL_ADMIN 3 -#define OUT_PATH_UNKNOWN 0xFF +#define OUT_PATH_FORCE_FLOOD 0xFE +#define OUT_PATH_UNKNOWN 0xFF struct ClientInfo { mesh::Identity id; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b78ad6ebd6..115865ec3d 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -4,6 +4,8 @@ #include "AdvertDataHelpers.h" #include "TxtDataHelpers.h" #include +#define STR_HELPER(x) #x +#define STR(x) STR_HELPER(x) #ifndef BRIDGE_MAX_BAUD #define BRIDGE_MAX_BAUD 115200 @@ -81,7 +83,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135 file.read((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136 file.read((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152 - file.read(pad, 3); // 153 + file.read((uint8_t *)&_prefs->reboot_interval, sizeof(_prefs->reboot_interval)); // 153 + file.read(pad, 2); // 154 file.read((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 file.read((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 file.read((uint8_t *)&_prefs->advert_loc_policy, sizeof (_prefs->advert_loc_policy)); // 161 @@ -91,7 +94,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 file.read((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 291 file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 - // next: 293 + file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 + // next: 294 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -115,12 +119,14 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->bridge_channel = constrain(_prefs->bridge_channel, 0, 14); _prefs->powersaving_enabled = constrain(_prefs->powersaving_enabled, 0, 1); + _prefs->reboot_interval = constrain(_prefs->reboot_interval, 0, 255); _prefs->gps_enabled = constrain(_prefs->gps_enabled, 0, 1); _prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2); // sanitise settings _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean + _prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean file.close(); } @@ -174,7 +180,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135 file.write((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136 file.write((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152 - file.write(pad, 3); // 153 + file.write((uint8_t *)&_prefs->reboot_interval, sizeof(_prefs->reboot_interval)); // 153 + file.write(pad, 2); // 154 file.write((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 file.write((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161 @@ -185,6 +192,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 291 file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 // next: 293 + file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 294 + // next: 295 file.close(); } @@ -455,6 +464,36 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { strcpy(reply, "off"); } + } else if (memcmp(command, "sensor", 6) == 0) { + // I2C +#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 + + // GPS +#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)); +#ifdef 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 (memcmp(command, "powerlog", 8) == 0) { + sprintf(reply, "Last reset reason: %s", _board->getResetReasonString(_board->getResetReason())); +#if defined(NRF52_PLATFORM) + sprintf(reply + strlen(reply), "\r\nLast shutdown reason: %s", + _board->getShutdownReasonString(_board->getShutdownReason())); + sprintf(reply + strlen(reply), "\r\nLast boot voltage: %u mV", _board->getBootVoltage()); +#endif } else if (memcmp(command, "log start", 9) == 0) { _callbacks->setLoggingOn(true); strcpy(reply, " logging on"); @@ -568,6 +607,28 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep savePrefs(); _callbacks->setRxBoostedGain(_prefs->rx_boosted_gain); #endif + } else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) { + if (!_board->canControlLoRaFemLna()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&config[17], "on", 2) == 0) { + if (_board->setLoRaFemLnaEnabled(true)) { + _prefs->radio_fem_rxgain = 1; + savePrefs(); + strcpy(reply, "OK - LoRa FEM RX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else if (memcmp(&config[17], "off", 3) == 0) { + if (_board->setLoRaFemLnaEnabled(false)) { + _prefs->radio_fem_rxgain = 0; + savePrefs(); + strcpy(reply, "OK - LoRa FEM RX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } } else if (memcmp(config, "radio ", 6) == 0) { strcpy(tmp, &config[6]); const char *parts[4]; @@ -759,6 +820,19 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->adc_multiplier = 0.0f; strcpy(reply, "Error: unsupported by this board"); }; + } else if (memcmp(config, "reboot.interval ", 16) == 0) { + int hours = _atoi(&config[16]); + if (hours == 0) { + _prefs->reboot_interval = 0; + savePrefs(); + strcpy(reply, "reboot.interval disabled"); + } else if (hours < 1 || 255 < hours) { + strcpy(reply, "Error: interval range is 1-255 hours"); + } else { + _prefs->reboot_interval = hours; + savePrefs(); + sprintf(reply, "OK - reboot.interval set to %d", _prefs->reboot_interval); + } } else { strcpy(reply, "unknown config: "); StrHelper::strncpy(&reply[16], config, 160-17); @@ -805,6 +879,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else if (memcmp(config, "radio.rxgain", 12) == 0) { sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off"); #endif + } else if (memcmp(config, "radio.fem.rxgain", 16) == 0) { + if (!_board->canControlLoRaFemLna()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off"); + } } else if (memcmp(config, "radio", 5) == 0) { char freq[16], bw[16]; strcpy(freq, StrHelper::ftoa(_prefs->freq)); @@ -926,6 +1006,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep #else strcpy(reply, "ERROR: Power management not supported"); #endif + } else if (memcmp(config, "reboot.interval", 15) == 0) { + if (_prefs->reboot_interval == 0) { + strcpy(reply, "disabled"); + } else { + sprintf(reply, "> %d", (uint8_t)_prefs->reboot_interval); + } } else { sprintf(reply, "??: %s", config); } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index b509c2b31a..095b640798 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -53,6 +53,7 @@ struct NodePrefs { // persisted to file char bridge_secret[16]; // for XOR encryption of bridge packets (ESP-NOW only) // Power setting uint8_t powersaving_enabled; // boolean + uint8_t reboot_interval; // hours, 0-255 (default 0=disable) // Gps settings uint8_t gps_enabled; uint32_t gps_interval; // in seconds @@ -61,6 +62,7 @@ struct NodePrefs { // persisted to file float adc_multiplier; char owner_info[120]; uint8_t rx_boosted_gain; // power settings + uint8_t radio_fem_rxgain; // LoRa FEM RX gain setting uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; }; diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index a4cbf2a980..058c800a9f 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -14,6 +14,7 @@ #include #include "soc/rtc.h" #include "esp_system.h" +#include class ESP32Board : public mesh::MainBoard { protected: @@ -62,6 +63,31 @@ class ESP32Board : public mesh::MainBoard { return raw / 4; } + void powerOff() override { + enterDeepSleep(0); // Do not wakeup + } + + void enterDeepSleep(uint32_t secs) { + // Clear stale wakeup sources to avoid ghost wakeup + // This is required when Power Management and automatic lightsleep are enabled + esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000ULL); + } + + // Keep LoRa inactive during deepsleep + digitalWrite(P_LORA_NSS, HIGH); +#if CONFIG_IDF_TARGET_ESP32C3 + gpio_hold_en((gpio_num_t)P_LORA_NSS); +#else + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); +#endif + + // Finally set ESP32 into deepsleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + uint32_t getIRQGpio() override { return P_LORA_DIO_1; // default for SX1262 } @@ -155,21 +181,68 @@ class ESP32Board : public mesh::MainBoard { void setInhibitSleep(bool inhibit) { inhibit_sleep = inhibit; } + + uint32_t getResetReason() const override { + return esp_reset_reason(); + } + + // https://docs.espressif.com/projects/esp-idf/en/v4.4.7/esp32/api-reference/system/system.html + const char *getResetReasonString(uint32_t reason) { + switch (reason) { + case ESP_RST_UNKNOWN: + return "Unknown or first boot"; + case ESP_RST_POWERON: + return "Power-on reset"; + case ESP_RST_EXT: + return "External reset"; + case ESP_RST_SW: + return "Software reset"; + case ESP_RST_PANIC: + return "Panic / exception reset"; + case ESP_RST_INT_WDT: + return "Interrupt watchdog reset"; + case ESP_RST_TASK_WDT: + return "Task watchdog reset"; + case ESP_RST_WDT: + return "Other watchdog reset"; + case ESP_RST_DEEPSLEEP: + return "Wake from deep sleep"; + case ESP_RST_BROWNOUT: + return "Brownout (low voltage)"; + case ESP_RST_SDIO: + return "SDIO reset"; + default: + static char buf[40]; + snprintf(buf, sizeof(buf), "Unknown reset reason (%d)", reason); + return buf; + } + } }; +static RTC_NOINIT_ATTR uint32_t _rtc_backup_time; +static RTC_NOINIT_ATTR uint32_t _rtc_backup_magic; +#define RTC_BACKUP_MAGIC 0xAA55CC33 +#define RTC_TIME_MIN 1772323200 // 1 Mar 2026 + class ESP32RTCClock : public mesh::RTCClock { public: ESP32RTCClock() { } void begin() { esp_reset_reason_t reason = esp_reset_reason(); - if (reason == ESP_RST_POWERON) { - // start with some date/time in the recent past - struct timeval tv; - tv.tv_sec = 1715770351; // 15 May 2024, 8:50pm + if (reason == ESP_RST_DEEPSLEEP) { + return; // ESP-IDF preserves system time across deep sleep + } + // All other resets (power-on, crash, WDT, brownout) lose system time. + // Restore from RTC backup if valid, otherwise use hardcoded seed. + struct timeval tv; + if (_rtc_backup_magic == RTC_BACKUP_MAGIC && _rtc_backup_time > RTC_TIME_MIN) { + tv.tv_sec = _rtc_backup_time; + } else { + tv.tv_sec = RTC_TIME_MIN; + } tv.tv_usec = 0; settimeofday(&tv, NULL); } - } uint32_t getCurrentTime() override { time_t _now; time(&_now); @@ -180,6 +253,16 @@ class ESP32RTCClock : public mesh::RTCClock { tv.tv_sec = time; tv.tv_usec = 0; settimeofday(&tv, NULL); + _rtc_backup_time = time; + _rtc_backup_magic = RTC_BACKUP_MAGIC; + } + void tick() override { + time_t now; + time(&now); + if (now > RTC_TIME_MIN && (uint32_t)now != _rtc_backup_time) { + _rtc_backup_time = (uint32_t)now; + _rtc_backup_magic = RTC_BACKUP_MAGIC; + } } }; diff --git a/src/helpers/MeshadventurerBoard.h b/src/helpers/MeshadventurerBoard.h index 65e1110294..0325161d5d 100644 --- a/src/helpers/MeshadventurerBoard.h +++ b/src/helpers/MeshadventurerBoard.h @@ -15,8 +15,6 @@ #include "ESP32Board.h" -#include - class MeshadventurerBoard : public ESP32Board { public: @@ -35,34 +33,6 @@ class MeshadventurerBoard : public ESP32Board { } } - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are held on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - - void powerOff() override { - // TODO: re-enable this when there is a definite wake-up source pin: - // enterDeepSleep(0); - } - uint16_t getBattMilliVolts() override { analogReadResolution(12); diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index dcfa0e1e34..50e1501e5e 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -182,6 +182,10 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { #define BLE_WRITE_MIN_INTERVAL 60 +bool SerialBLEInterface::isReadBusy() const { + return (recv_queue_len > 0); +} + bool SerialBLEInterface::isWriteBusy() const { return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write? } diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 965e90fd19..19e024b040 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -76,6 +76,7 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE bool isConnected() const override; + bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; diff --git a/src/helpers/esp32/SerialWifiInterface.cpp b/src/helpers/esp32/SerialWifiInterface.cpp index 462e3ecc30..bdecb1a9dd 100644 --- a/src/helpers/esp32/SerialWifiInterface.cpp +++ b/src/helpers/esp32/SerialWifiInterface.cpp @@ -39,6 +39,10 @@ size_t SerialWifiInterface::writeFrame(const uint8_t src[], size_t len) { return 0; } +bool SerialWifiInterface::isReadBusy() const { + return false; +} + bool SerialWifiInterface::isWriteBusy() const { return false; } diff --git a/src/helpers/esp32/SerialWifiInterface.h b/src/helpers/esp32/SerialWifiInterface.h index 19291497fe..1ff1d83d25 100644 --- a/src/helpers/esp32/SerialWifiInterface.h +++ b/src/helpers/esp32/SerialWifiInterface.h @@ -52,6 +52,7 @@ class SerialWifiInterface : public BaseSerialInterface { bool isEnabled() const override { return _isEnabled; } bool isConnected() const override; + bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; diff --git a/src/helpers/esp32/TBeamBoard.h b/src/helpers/esp32/TBeamBoard.h index 98bd16bff4..543226d662 100644 --- a/src/helpers/esp32/TBeamBoard.h +++ b/src/helpers/esp32/TBeamBoard.h @@ -86,7 +86,6 @@ #include #include "XPowersLib.h" #include "helpers/ESP32Board.h" -#include class TBeamBoard : public ESP32Board { XPowersLibInterface *PMU = NULL; @@ -131,29 +130,6 @@ bool power_init(); } #endif - void enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! -} - uint16_t getBattMilliVolts(){ return PMU->getBattVoltage(); } diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index 75a4e3b064..a846e744ed 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -401,6 +401,10 @@ bool SerialBLEInterface::isConnected() const { return _isDeviceConnected && Bluefruit.connected() > 0; } +bool SerialBLEInterface::isReadBusy() const { + return (recv_queue_len > 0); +} + bool SerialBLEInterface::isWriteBusy() const { return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3); } diff --git a/src/helpers/nrf52/SerialBLEInterface.h b/src/helpers/nrf52/SerialBLEInterface.h index de1030548f..d3cc505516 100644 --- a/src/helpers/nrf52/SerialBLEInterface.h +++ b/src/helpers/nrf52/SerialBLEInterface.h @@ -66,6 +66,7 @@ class SerialBLEInterface : public BaseSerialInterface { void disable() override; bool isEnabled() const override { return _isEnabled; } bool isConnected() const override; + bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 73842d9eeb..637d61afae 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -40,6 +40,7 @@ static uint32_t bsec_last_save_ms = 0; #ifdef ENV_INCLUDE_BME680 #ifndef TELEM_BME680_ADDRESS #define TELEM_BME680_ADDRESS 0x76 +#define TELEM_BME680_ADDRESS_2 0x77 #endif #define TELEM_BME680_SEALEVELPRESSURE_HPA (1013.25) #include @@ -63,6 +64,7 @@ static Adafruit_AHTX0 AHTX0; #if ENV_INCLUDE_BME280 #ifndef TELEM_BME280_ADDRESS #define TELEM_BME280_ADDRESS 0x76 // BME280 environmental sensor I2C address +#define TELEM_BME280_ADDRESS_2 0x77 #endif #define TELEM_BME280_SEALEVELPRESSURE_HPA (1013.25) // Atmospheric pressure at sea level #include @@ -72,6 +74,7 @@ static Adafruit_BME280 BME280; #if ENV_INCLUDE_BMP280 #ifndef TELEM_BMP280_ADDRESS #define TELEM_BMP280_ADDRESS 0x76 // BMP280 environmental sensor I2C address +#define TELEM_BMP280_ADDRESS_2 0x77 #endif #define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Atmospheric pressure at sea level #include @@ -557,15 +560,27 @@ static const SensorDef SENSOR_TABLE[] = { #endif #ifdef ENV_INCLUDE_BME680 { TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 }, + #ifdef TELEM_BME680_ADDRESS_2 + { TELEM_BME680_ADDRESS_2, "BME680", init_bme680, query_bme680 }, + #endif #endif #if ENV_INCLUDE_BME680_BSEC { TELEM_BME680_ADDRESS, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec }, + #ifdef TELEM_BME680_ADDRESS_2 + { TELEM_BME680_ADDRESS_2, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec }, + #endif #endif #if ENV_INCLUDE_BME280 { TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 }, + #ifdef TELEM_BME280_ADDRESS_2 + { TELEM_BME280_ADDRESS_2, "BME280", init_bme280, query_bme280 }, + #endif #endif #if ENV_INCLUDE_BMP280 { TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 }, + #ifdef TELEM_BMP280_ADDRESS_2 + { TELEM_BMP280_ADDRESS_2, "BMP280", init_bmp280, query_bmp280 }, + #endif #endif #if ENV_INCLUDE_SHTC3 { 0x70, "SHTC3", init_shtc3, query_shtc3 }, diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index eec466d3aa..8b3c58679a 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -76,6 +76,7 @@ public : void begin() override { claim(); if (_pin_en != -1) { + pinMode(_pin_en, OUTPUT); digitalWrite(_pin_en, PIN_GPS_EN_ACTIVE); } if (_pin_reset != -1) { @@ -94,6 +95,7 @@ public : void stop() override { if (_pin_en != -1) { digitalWrite(_pin_en, !PIN_GPS_EN_ACTIVE); + pinMode(_pin_en, INPUT); // Reduce 0.3mA leaking } if (_pin_reset != -1) { digitalWrite(_pin_reset, GPS_RESET_FORCE); diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index a6087dd813..8983c91148 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -63,6 +63,15 @@ void ST7735Display::turnOff() { #else digitalWrite(PIN_TFT_LEDA_CTL, LOW); #endif + + // Prevent back-powering to save 2.8 mA + pinMode(PIN_TFT_CS, INPUT); + pinMode(PIN_TFT_DC, INPUT); + pinMode(PIN_TFT_SDA, INPUT); + pinMode(PIN_TFT_SCL, INPUT); + pinMode(PIN_TFT_RST, INPUT); + pinMode(PIN_TFT_LEDA_CTL, INPUT); + _isOn = false; if (_peripher_power) _peripher_power->release(); diff --git a/variants/gat562_30s_mesh_kit/platformio.ini b/variants/gat562_30s_mesh_kit/platformio.ini index 2baac2561b..aa3915a4cd 100644 --- a/variants/gat562_30s_mesh_kit/platformio.ini +++ b/variants/gat562_30s_mesh_kit/platformio.ini @@ -2,12 +2,13 @@ extends = nrf52_base board = rak4631 board_check = true +board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_30s_mesh_kit -D RAK_4631 -D RAK_BOARD - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/gat562_mesh_evb_pro/platformio.ini b/variants/gat562_mesh_evb_pro/platformio.ini index b3e894174a..d7de585ad3 100644 --- a/variants/gat562_mesh_evb_pro/platformio.ini +++ b/variants/gat562_mesh_evb_pro/platformio.ini @@ -5,7 +5,7 @@ board_check = true build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_evb_pro - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D RADIO_CLASS=CustomSX1262 diff --git a/variants/gat562_mesh_tracker_pro/platformio.ini b/variants/gat562_mesh_tracker_pro/platformio.ini index af153b8fc2..78ec7d0189 100644 --- a/variants/gat562_mesh_tracker_pro/platformio.ini +++ b/variants/gat562_mesh_tracker_pro/platformio.ini @@ -2,10 +2,11 @@ extends = nrf52_base board = rak4631 board_check = true +board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_tracker_pro - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/gat562_mesh_watch13/platformio.ini b/variants/gat562_mesh_watch13/platformio.ini index f3510b74aa..f457424f10 100644 --- a/variants/gat562_mesh_watch13/platformio.ini +++ b/variants/gat562_mesh_watch13/platformio.ini @@ -8,7 +8,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/gat562_mesh_watch13 -D RAK_4631 -D RAK_BOARD - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/heltec_ct62/platformio.ini b/variants/heltec_ct62/platformio.ini index 0179d9658c..e8becc7e55 100644 --- a/variants/heltec_ct62/platformio.ini +++ b/variants/heltec_ct62/platformio.ini @@ -130,6 +130,10 @@ lib_deps = ${esp32_ota.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_ct62_companion_radio_ble_ps] +extends = env:Heltec_ct62_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 + [env:Heltec_ct62_sensor] extends = Heltec_ct62 build_flags = diff --git a/variants/heltec_e213/HeltecE213Board.cpp b/variants/heltec_e213/HeltecE213Board.cpp index af11531821..88737c4d53 100644 --- a/variants/heltec_e213/HeltecE213Board.cpp +++ b/variants/heltec_e213/HeltecE213Board.cpp @@ -20,33 +20,6 @@ void HeltecE213Board::begin() { } } - void HeltecE213Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - - void HeltecE213Board::powerOff() { - enterDeepSleep(0); - } - uint16_t HeltecE213Board::getBattMilliVolts() { analogReadResolution(10); digitalWrite(PIN_ADC_CTRL, HIGH); diff --git a/variants/heltec_e213/HeltecE213Board.h b/variants/heltec_e213/HeltecE213Board.h index 2192c14104..fadc038f94 100644 --- a/variants/heltec_e213/HeltecE213Board.h +++ b/variants/heltec_e213/HeltecE213Board.h @@ -3,7 +3,6 @@ #include #include #include -#include class HeltecE213Board : public ESP32Board { @@ -13,8 +12,6 @@ class HeltecE213Board : public ESP32Board { HeltecE213Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { } void begin(); - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); - void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; }; diff --git a/variants/heltec_e290/HeltecE290Board.cpp b/variants/heltec_e290/HeltecE290Board.cpp index 3994a20616..96ec59c9bd 100644 --- a/variants/heltec_e290/HeltecE290Board.cpp +++ b/variants/heltec_e290/HeltecE290Board.cpp @@ -20,33 +20,6 @@ void HeltecE290Board::begin() { } } - void HeltecE290Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - - void HeltecE290Board::powerOff() { - enterDeepSleep(0); - } - uint16_t HeltecE290Board::getBattMilliVolts() { analogReadResolution(10); digitalWrite(PIN_ADC_CTRL, HIGH); diff --git a/variants/heltec_e290/HeltecE290Board.h b/variants/heltec_e290/HeltecE290Board.h index 645ec3481a..f287227c6e 100644 --- a/variants/heltec_e290/HeltecE290Board.h +++ b/variants/heltec_e290/HeltecE290Board.h @@ -3,7 +3,6 @@ #include #include #include -#include class HeltecE290Board : public ESP32Board { @@ -13,8 +12,6 @@ class HeltecE290Board : public ESP32Board { HeltecE290Board() : periph_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } void begin(); - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); - void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/heltec_t096/LoRaFEMControl.h b/variants/heltec_t096/LoRaFEMControl.h index 2c50b74289..a3b5c4ed9b 100644 --- a/variants/heltec_t096/LoRaFEMControl.h +++ b/variants/heltec_t096/LoRaFEMControl.h @@ -12,10 +12,11 @@ class LoRaFEMControl void setRxModeEnable(void); void setRxModeEnableWhenMCUSleep(void); void setLNAEnable(bool enabled); - bool isLnaCanControl(void) { return lna_can_control; } + bool isLnaCanControl(void) const { return lna_can_control; } void setLnaCanControl(bool can_control) { lna_can_control = can_control; } + bool isLNAEnabled(void) const { return lna_enabled; } private: - bool lna_enabled = false; + bool lna_enabled = true; bool lna_can_control = false; }; diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 550131571f..c7f6972209 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -123,4 +123,26 @@ void T096Board::powerOff() { const char* T096Board::getManufacturerName() const { return "Heltec T096"; -} \ No newline at end of file +} + +bool T096Board::setLoRaFemLnaEnabled(bool enable) { +#if defined(RADIO_FEM_RXGAIN) && (RADIO_FEM_RXGAIN == 0) + enable = false; +#endif + + if (!loRaFEMControl.isLnaCanControl()) { + return false; + } + + loRaFEMControl.setLNAEnable(enable); + loRaFEMControl.setRxModeEnable(); + return true; +} + +bool T096Board::canControlLoRaFemLna() const { + return loRaFEMControl.isLnaCanControl(); +} + +bool T096Board::isLoRaFemLnaEnabled() const { + return loRaFEMControl.isLNAEnabled(); +} diff --git a/variants/heltec_t096/T096Board.h b/variants/heltec_t096/T096Board.h index d1e3bdfdee..15c7e68b5d 100644 --- a/variants/heltec_t096/T096Board.h +++ b/variants/heltec_t096/T096Board.h @@ -25,4 +25,7 @@ class T096Board : public NRF52BoardDCDC { uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; void powerOff() override; + bool setLoRaFemLnaEnabled(bool enable) override; + bool canControlLoRaFemLna() const override; + bool isLoRaFemLnaEnabled() const override; }; diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index e820bf58d3..a0e20ef629 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/heltec_t096 -I src/helpers/ui -D HELTEC_T096 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=21 -D P_LORA_NSS=5 -D P_LORA_RESET=16 @@ -143,6 +143,12 @@ lib_deps = ${Heltec_t096.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_t096_companion_radio_ble_femoff] +extends = env:Heltec_t096_companion_radio_ble +build_flags = + ${env:Heltec_t096_companion_radio_ble.build_flags} + -D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off + [env:Heltec_t096_companion_radio_usb] extends = Heltec_t096 board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index 135babb1a2..e808d6c287 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -12,7 +12,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/heltec_t114 -I src/helpers/ui -D HELTEC_T114 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=20 -D P_LORA_NSS=24 -D P_LORA_RESET=25 diff --git a/variants/heltec_t190/HeltecT190Board.cpp b/variants/heltec_t190/HeltecT190Board.cpp index 4f35be400b..0a16b52b8d 100644 --- a/variants/heltec_t190/HeltecT190Board.cpp +++ b/variants/heltec_t190/HeltecT190Board.cpp @@ -20,33 +20,6 @@ void HeltecT190Board::begin() { } } - void HeltecT190Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - - void HeltecT190Board::powerOff() { - enterDeepSleep(0); - } - uint16_t HeltecT190Board::getBattMilliVolts() { analogReadResolution(10); digitalWrite(PIN_ADC_CTRL, HIGH); diff --git a/variants/heltec_t190/HeltecT190Board.h b/variants/heltec_t190/HeltecT190Board.h index bc38c1e090..557c070ecd 100644 --- a/variants/heltec_t190/HeltecT190Board.h +++ b/variants/heltec_t190/HeltecT190Board.h @@ -3,7 +3,6 @@ #include #include #include -#include class HeltecT190Board : public ESP32Board { @@ -13,8 +12,6 @@ class HeltecT190Board : public ESP32Board { HeltecT190Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { } void begin(); - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); - void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/heltec_tracker/platformio.ini b/variants/heltec_tracker/platformio.ini index 69293d7070..2cd0cea6a8 100644 --- a/variants/heltec_tracker/platformio.ini +++ b/variants/heltec_tracker/platformio.ini @@ -99,6 +99,10 @@ lib_deps = ${Heltec_tracker_base.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_Wireless_Tracker_companion_radio_ble_ps] +extends = env:Heltec_Wireless_Tracker_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 + [env:Heltec_Wireless_Tracker_repeater] extends = Heltec_tracker_base build_flags = diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp index aabfed7967..99b1cdfe08 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp @@ -35,33 +35,12 @@ void HeltecTrackerV2Board::begin() { loRaFEMControl.setRxModeEnable(); } - void HeltecTrackerV2Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + void HeltecTrackerV2Board::powerOff() { + // Turn off PA + digitalWrite(P_LORA_PA_POWER, LOW); + rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_POWER); - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - loRaFEMControl.setRxModeEnableWhenMCUSleep();//It also needs to be enabled in receive mode - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - - void HeltecTrackerV2Board::powerOff() { - enterDeepSleep(0); + ESP32Board::powerOff(); } uint16_t HeltecTrackerV2Board::getBattMilliVolts() { @@ -82,3 +61,21 @@ void HeltecTrackerV2Board::begin() { const char* HeltecTrackerV2Board::getManufacturerName() const { return "Heltec Tracker V2"; } + + bool HeltecTrackerV2Board::setLoRaFemLnaEnabled(bool enable) { + if (!loRaFEMControl.isLnaCanControl()) { + return false; + } + + loRaFEMControl.setLNAEnable(enable); + loRaFEMControl.setRxModeEnable(); + return true; + } + + bool HeltecTrackerV2Board::canControlLoRaFemLna() const { + return loRaFEMControl.isLnaCanControl(); + } + + bool HeltecTrackerV2Board::isLoRaFemLnaEnabled() const { + return loRaFEMControl.isLNAEnabled(); + } diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h index 33c897bc94..2bd6a02544 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h @@ -3,7 +3,6 @@ #include #include #include -#include #include "LoRaFEMControl.h" class HeltecTrackerV2Board : public ESP32Board { @@ -17,9 +16,11 @@ class HeltecTrackerV2Board : public ESP32Board { void begin(); void onBeforeTransmit(void) override; void onAfterTransmit(void) override; - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; + bool setLoRaFemLnaEnabled(bool enable) override; + bool canControlLoRaFemLna() const override; + bool isLoRaFemLnaEnabled() const override; }; diff --git a/variants/heltec_tracker_v2/LoRaFEMControl.h b/variants/heltec_tracker_v2/LoRaFEMControl.h index 2c50b74289..a3b5c4ed9b 100644 --- a/variants/heltec_tracker_v2/LoRaFEMControl.h +++ b/variants/heltec_tracker_v2/LoRaFEMControl.h @@ -12,10 +12,11 @@ class LoRaFEMControl void setRxModeEnable(void); void setRxModeEnableWhenMCUSleep(void); void setLNAEnable(bool enabled); - bool isLnaCanControl(void) { return lna_can_control; } + bool isLnaCanControl(void) const { return lna_can_control; } void setLnaCanControl(bool can_control) { lna_can_control = can_control; } + bool isLNAEnabled(void) const { return lna_enabled; } private: - bool lna_enabled = false; + bool lna_enabled = true; bool lna_can_control = false; }; diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index d57c2113f0..d914ce6a1b 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -176,6 +176,10 @@ lib_deps = ${Heltec_tracker_v2.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:heltec_tracker_v2_companion_radio_ble_ps] +extends = env:heltec_tracker_v2_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 + [env:heltec_tracker_v2_companion_radio_wifi] extends = Heltec_tracker_v2 build_flags = diff --git a/variants/heltec_v2/HeltecV2Board.h b/variants/heltec_v2/HeltecV2Board.h index fe800890b8..9b08fe9430 100644 --- a/variants/heltec_v2/HeltecV2Board.h +++ b/variants/heltec_v2/HeltecV2Board.h @@ -7,8 +7,6 @@ #define PIN_VBAT_READ 37 #define PIN_LED_BUILTIN 25 -#include - class HeltecV2Board : public ESP32Board { public: void begin() { @@ -26,29 +24,6 @@ class HeltecV2Board : public ESP32Board { } } - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_0, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_0); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_0), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_0) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - uint16_t getBattMilliVolts() override { analogReadResolution(10); diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index ba4f869422..e9cf56f053 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -172,6 +172,10 @@ lib_deps = ${Heltec_lora32_v2.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_v2_companion_radio_ble_ps] +extends = env:Heltec_v2_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 + [env:Heltec_v2_companion_radio_wifi] extends = Heltec_lora32_v2 build_flags = diff --git a/variants/heltec_v3/HeltecV3Board.h b/variants/heltec_v3/HeltecV3Board.h index ba22a7f2b9..7e7abe313b 100644 --- a/variants/heltec_v3/HeltecV3Board.h +++ b/variants/heltec_v3/HeltecV3Board.h @@ -17,8 +17,6 @@ #define PIN_ADC_CTRL_ACTIVE LOW #define PIN_ADC_CTRL_INACTIVE HIGH -#include - class HeltecV3Board : public ESP32Board { private: bool adc_active_state; @@ -52,33 +50,6 @@ class HeltecV3Board : public ESP32Board { } } - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - - void powerOff() override { - enterDeepSleep(0); - } - uint16_t getBattMilliVolts() override { analogReadResolution(10); digitalWrite(PIN_ADC_CTRL, adc_active_state); diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index a70a93a508..69e4ed050b 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -179,6 +179,10 @@ lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_v3_companion_radio_ble_ps] +extends = env:Heltec_v3_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 + [env:Heltec_v3_companion_radio_wifi] extends = Heltec_lora32_v3 build_flags = @@ -320,6 +324,10 @@ lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_WSL3_companion_radio_ble_ps] +extends = env:Heltec_WSL3_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 + [env:Heltec_WSL3_companion_radio_usb] extends = Heltec_lora32_v3 build_flags = diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index 8f01379714..ba3a7f1c3d 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -32,33 +32,12 @@ void HeltecV4Board::begin() { loRaFEMControl.setRxModeEnable(); } - void HeltecV4Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + void HeltecV4Board::powerOff() { + // Turn off PA + digitalWrite(P_LORA_PA_POWER, LOW); + rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_POWER); - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - loRaFEMControl.setRxModeEnableWhenMCUSleep();//It also needs to be enabled in receive mode - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - - void HeltecV4Board::powerOff() { - enterDeepSleep(0); + ESP32Board::powerOff(); } uint16_t HeltecV4Board::getBattMilliVolts() { @@ -83,3 +62,25 @@ void HeltecV4Board::begin() { return loRaFEMControl.getFEMType() == KCT8103L_PA ? "Heltec V4.3 OLED" : "Heltec V4 OLED"; #endif } + + bool HeltecV4Board::setLoRaFemLnaEnabled(bool enable) { +#if defined(RADIO_FEM_RXGAIN) && (RADIO_FEM_RXGAIN == 0) + enable = false; +#endif + + if (!loRaFEMControl.isLnaCanControl()) { + return false; + } + + loRaFEMControl.setLNAEnable(enable); + loRaFEMControl.setRxModeEnable(); + return true; + } + + bool HeltecV4Board::canControlLoRaFemLna() const { + return loRaFEMControl.isLnaCanControl(); + } + + bool HeltecV4Board::isLoRaFemLnaEnabled() const { + return loRaFEMControl.isLNAEnabled(); + } diff --git a/variants/heltec_v4/HeltecV4Board.h b/variants/heltec_v4/HeltecV4Board.h index 95def06c95..55166bb37f 100644 --- a/variants/heltec_v4/HeltecV4Board.h +++ b/variants/heltec_v4/HeltecV4Board.h @@ -3,7 +3,6 @@ #include #include #include -#include #include "LoRaFEMControl.h" #ifndef ADC_MULTIPLIER @@ -23,8 +22,10 @@ class HeltecV4Board : public ESP32Board { void begin(); void onBeforeTransmit(void) override; void onAfterTransmit(void) override; - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); void powerOff() override; + bool setLoRaFemLnaEnabled(bool enable) override; + bool canControlLoRaFemLna() const override; + bool isLoRaFemLnaEnabled() const override; uint16_t getBattMilliVolts() override; bool setAdcMultiplier(float multiplier) override { if (multiplier == 0.0f) { diff --git a/variants/heltec_v4/LoRaFEMControl.h b/variants/heltec_v4/LoRaFEMControl.h index 13225bd56b..d84ebe9c6a 100644 --- a/variants/heltec_v4/LoRaFEMControl.h +++ b/variants/heltec_v4/LoRaFEMControl.h @@ -18,12 +18,13 @@ class LoRaFEMControl void setRxModeEnable(void); void setRxModeEnableWhenMCUSleep(void); void setLNAEnable(bool enabled); - bool isLnaCanControl(void) { return lna_can_control; } + bool isLnaCanControl(void) const { return lna_can_control; } void setLnaCanControl(bool can_control) { lna_can_control = can_control; } + bool isLNAEnabled(void) const { return lna_enabled; } LoRaFEMType getFEMType(void) const { return fem_type; } private: LoRaFEMType fem_type=OTHER_FEM_TYPES; - bool lna_enabled=false; + bool lna_enabled=true; bool lna_can_control=false; }; diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index fabf38272d..5ad2b1e979 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -222,6 +222,16 @@ lib_deps = ${heltec_v4_oled.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:heltec_v4_companion_radio_ble_ps] +extends = env:heltec_v4_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 + +[env:heltec_v4_3_companion_radio_ble_ps_femoff] +extends = env:heltec_v4_companion_radio_ble_ps +build_flags = + ${env:heltec_v4_companion_radio_ble_ps.build_flags} + -D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off + [env:heltec_v4_companion_radio_wifi] extends = heltec_v4_oled build_flags = @@ -386,6 +396,14 @@ lib_deps = ${heltec_v4_tft.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:heltec_v4_expansionkit_tft_companion_radio_ble_ps] +extends = env:heltec_v4_tft_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${env:heltec_v4_tft_companion_radio_ble.build_flags} + -D ENV_PIN_SDA=4 + -D ENV_PIN_SCL=3 + [env:heltec_v4_tft_companion_radio_wifi] extends = heltec_v4_tft build_flags = diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index 48723d169a..e7a107a5e9 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -64,6 +64,10 @@ lib_deps = densaugeo/base64 @ ~1.4.0 bakercp/CRC32 @ ^2.0.0 +[env:Heltec_Wireless_Paper_companion_radio_ble_ps] +extends = env:Heltec_Wireless_Paper_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 + [env:Heltec_Wireless_Paper_companion_radio_usb] extends = Heltec_Wireless_Paper_base build_flags = diff --git a/variants/lilygo_t3s3/platformio.ini b/variants/lilygo_t3s3/platformio.ini index 54990117cc..e7f9f11f95 100644 --- a/variants/lilygo_t3s3/platformio.ini +++ b/variants/lilygo_t3s3/platformio.ini @@ -174,6 +174,10 @@ lib_deps = ${LilyGo_T3S3_sx1262.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:LilyGo_T3S3_sx1262_companion_radio_ble_ps] +extends = env:LilyGo_T3S3_sx1262_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 + [env:LilyGo_T3S3_sx1262_kiss_modem] extends = LilyGo_T3S3_sx1262 build_src_filter = ${LilyGo_T3S3_sx1262.build_src_filter} diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index c7a595520e..4516e4cd7b 100644 --- a/variants/lilygo_tbeam_1w/platformio.ini +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -146,6 +146,11 @@ lib_deps = ${LilyGo_TBeam_1W.lib_deps} densaugeo/base64 @ ~1.4.0 +; === LILYGO T-Beam 1W Companion Radio PS (BLE PS) === +[env:LilyGo_TBeam_1W_companion_radio_ble_ps] +extends = env:LilyGo_TBeam_1W_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 + ; === LILYGO T-Beam 1W Companion Radio (WiFi) === [env:LilyGo_TBeam_1W_companion_radio_wifi] extends = LilyGo_TBeam_1W diff --git a/variants/lilygo_tbeam_SX1262/platformio.ini b/variants/lilygo_tbeam_SX1262/platformio.ini index 62ac09f875..5bc0efddb3 100644 --- a/variants/lilygo_tbeam_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_SX1262/platformio.ini @@ -65,6 +65,13 @@ lib_deps = ${LilyGo_TBeam_SX1262.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Tbeam_SX1262_companion_radio_ble_ps] +extends = env:Tbeam_SX1262_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${env:Tbeam_SX1262_companion_radio_ble.build_flags} + -D MAX_CONTACTS=150 + [env:Tbeam_SX1262_repeater] extends = LilyGo_TBeam_SX1262 build_flags = diff --git a/variants/lilygo_tbeam_SX1276/platformio.ini b/variants/lilygo_tbeam_SX1276/platformio.ini index cb25903ce9..edf84e21bf 100644 --- a/variants/lilygo_tbeam_SX1276/platformio.ini +++ b/variants/lilygo_tbeam_SX1276/platformio.ini @@ -61,6 +61,10 @@ lib_deps = ${LilyGo_TBeam_SX1276.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Tbeam_SX1276_companion_radio_ble_ps] +extends = env:Tbeam_SX1276_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 + [env:Tbeam_SX1276_repeater] extends = LilyGo_TBeam_SX1276 build_flags = diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index 0294615651..b930a25b93 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -142,6 +142,10 @@ lib_deps = ${T_Beam_S3_Supreme_SX1262.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:T_Beam_S3_Supreme_SX1262_companion_radio_ble_ps] +extends = env:T_Beam_S3_Supreme_SX1262_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 + [env:T_Beam_S3_Supreme_SX1262_companion_radio_wifi] extends = T_Beam_S3_Supreme_SX1262 build_flags = diff --git a/variants/lilygo_tdeck/TDeckBoard.h b/variants/lilygo_tdeck/TDeckBoard.h index 7ed007af9c..e2844360df 100644 --- a/variants/lilygo_tdeck/TDeckBoard.h +++ b/variants/lilygo_tdeck/TDeckBoard.h @@ -3,7 +3,6 @@ #include #include #include "helpers/ESP32Board.h" -#include #define PIN_VBAT_READ 4 #define BATTERY_SAMPLES 8 @@ -23,29 +22,6 @@ class TDeckBoard : public ESP32Board { } #endif - void enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - uint16_t getBattMilliVolts() { #if defined(PIN_VBAT_READ) && defined(ADC_MULTIPLIER) analogReadResolution(12); diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index 3673166861..bb173524c4 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -92,7 +92,7 @@ extends = LilyGo_TLora_V2_1_1_6 build_flags = ${LilyGo_TLora_V2_1_1_6.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=160 + -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 -D OFFLINE_QUEUE_SIZE=128 @@ -108,6 +108,10 @@ lib_deps = ${LilyGo_TLora_V2_1_1_6.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:LilyGo_TLora_V2_1_1_6_companion_radio_ble_ps] +extends = env:LilyGo_TLora_V2_1_1_6_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 + [env:LilyGo_TLora_V2_1_1_6_room_server] extends = LilyGo_TLora_V2_1_1_6 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter} diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index 5415e15861..1e1a32740f 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -1,6 +1,7 @@ [Promicro] extends = nrf52_base board = promicro_nrf52840 +board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} -I variants/promicro -D PROMICRO diff --git a/variants/rak3112/RAK3112Board.h b/variants/rak3112/RAK3112Board.h index 8ba3197cf6..704162b8c8 100644 --- a/variants/rak3112/RAK3112Board.h +++ b/variants/rak3112/RAK3112Board.h @@ -16,8 +16,6 @@ #define ADC_MULTIPLIER (3 * 1.73 * 1.187 * 1000) #define BATTERY_SAMPLES 8 -#include - class RAK3112Board : public ESP32Board { private: bool adc_active_state; @@ -51,33 +49,6 @@ class RAK3112Board : public ESP32Board { } } - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - - void powerOff() override { - enterDeepSleep(0); - } - uint16_t getBattMilliVolts() override { analogReadResolution(12); diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 20a8a548b9..5eeb6a9d6c 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -2,11 +2,12 @@ extends = nrf52_base board = rak3401 board_check = true +board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak3401 -D RAK_3401 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 2bbba31463..8f72999af4 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -2,6 +2,7 @@ extends = nrf52_base board = rak4631 board_check = true +board_build.ldscript = boards/nrf52840_s140_v6.ld extra_scripts = ${nrf52_base.extra_scripts} post:variants/rak4631/fix_bsec_lib.py build_flags = ${nrf52_base.build_flags} @@ -9,7 +10,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/rak4631 -D RAK_4631 -D RAK_BOARD - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_GPS_TX=PIN_SERIAL1_RX diff --git a/variants/rak_wismesh_tag/platformio.ini b/variants/rak_wismesh_tag/platformio.ini index e9cddb74dd..b596c9a659 100644 --- a/variants/rak_wismesh_tag/platformio.ini +++ b/variants/rak_wismesh_tag/platformio.ini @@ -2,6 +2,7 @@ extends = nrf52_base board = rak4631 board_check = true +board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak_wismesh_tag diff --git a/variants/sensecap_solar/platformio.ini b/variants/sensecap_solar/platformio.ini index effef38ccd..c9f7ed397d 100644 --- a/variants/sensecap_solar/platformio.ini +++ b/variants/sensecap_solar/platformio.ini @@ -10,7 +10,7 @@ build_flags = ${nrf52_base.build_flags} -I src/helpers/nrf52 -D NRF52_PLATFORM=1 -D USE_SX1262 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D P_LORA_TX_LED=12 diff --git a/variants/station_g2/StationG2Board.h b/variants/station_g2/StationG2Board.h index a905682c8d..d1989ee0ff 100644 --- a/variants/station_g2/StationG2Board.h +++ b/variants/station_g2/StationG2Board.h @@ -2,7 +2,6 @@ #include #include -#include class StationG2Board : public ESP32Board { public: @@ -21,29 +20,6 @@ class StationG2Board : public ESP32Board { } } - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - uint16_t getBattMilliVolts() override { return 0; } diff --git a/variants/thinknode_m2/ThinknodeM2Board.cpp b/variants/thinknode_m2/ThinknodeM2Board.cpp index 0596510361..8d68006d2c 100644 --- a/variants/thinknode_m2/ThinknodeM2Board.cpp +++ b/variants/thinknode_m2/ThinknodeM2Board.cpp @@ -1,40 +1,30 @@ #include "ThinknodeM2Board.h" - - void ThinknodeM2Board::begin() { - pinMode(PIN_VEXT_EN, OUTPUT); - digitalWrite(PIN_VEXT_EN, !PIN_VEXT_EN_ACTIVE); // force power cycle - delay(20); // allow power rail to discharge - digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE); // turn backlight back on - delay(120); // give display time to bias on cold boot - ESP32Board::begin(); - pinMode(PIN_STATUS_LED, OUTPUT); // init power led - } + pinMode(PIN_VEXT_EN, OUTPUT); + digitalWrite(PIN_VEXT_EN, !PIN_VEXT_EN_ACTIVE); // force power cycle + delay(20); // allow power rail to discharge + digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE); // turn backlight back on + delay(120); // give display time to bias on cold boot + ESP32Board::begin(); + pinMode(PIN_STATUS_LED, OUTPUT); // init power led +} - void ThinknodeM2Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_deep_sleep_start(); - } +uint16_t ThinknodeM2Board::getBattMilliVolts() { + analogReadResolution(12); + analogSetPinAttenuation(PIN_VBAT_READ, ADC_11db); - void ThinknodeM2Board::powerOff() { - enterDeepSleep(0); + uint32_t mv = 0; + for (int i = 0; i < 8; ++i) { + mv += analogReadMilliVolts(PIN_VBAT_READ); + delayMicroseconds(200); } + mv /= 8; - uint16_t ThinknodeM2Board::getBattMilliVolts() { - analogReadResolution(12); - analogSetPinAttenuation(PIN_VBAT_READ, ADC_11db); - - uint32_t mv = 0; - for (int i = 0; i < 8; ++i) { - mv += analogReadMilliVolts(PIN_VBAT_READ); - delayMicroseconds(200); - } - mv /= 8; - - analogReadResolution(10); - return static_cast(mv * ADC_MULTIPLIER ); + analogReadResolution(10); + return static_cast(mv * ADC_MULTIPLIER); } - const char* ThinknodeM2Board::getManufacturerName() const { - return "Elecrow ThinkNode M2"; - } +const char *ThinknodeM2Board::getManufacturerName() const { + return "Elecrow ThinkNode M2"; +} diff --git a/variants/thinknode_m2/ThinknodeM2Board.h b/variants/thinknode_m2/ThinknodeM2Board.h index 8011fae67d..0255677784 100644 --- a/variants/thinknode_m2/ThinknodeM2Board.h +++ b/variants/thinknode_m2/ThinknodeM2Board.h @@ -3,15 +3,11 @@ #include #include #include -#include class ThinknodeM2Board : public ESP32Board { public: - void begin(); - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); - void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/thinknode_m5/ThinknodeM5Board.cpp b/variants/thinknode_m5/ThinknodeM5Board.cpp index c4de538c56..2cb138e639 100644 --- a/variants/thinknode_m5/ThinknodeM5Board.cpp +++ b/variants/thinknode_m5/ThinknodeM5Board.cpp @@ -19,14 +19,6 @@ void ThinknodeM5Board::begin() { ESP32Board::begin(); } - void ThinknodeM5Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_deep_sleep_start(); - } - - void ThinknodeM5Board::powerOff() { - enterDeepSleep(0); - } - uint16_t ThinknodeM5Board::getBattMilliVolts() { analogReadResolution(12); analogSetPinAttenuation(PIN_VBAT_READ, ADC_11db); diff --git a/variants/thinknode_m5/ThinknodeM5Board.h b/variants/thinknode_m5/ThinknodeM5Board.h index 3c120027b7..57ff0d00eb 100644 --- a/variants/thinknode_m5/ThinknodeM5Board.h +++ b/variants/thinknode_m5/ThinknodeM5Board.h @@ -3,7 +3,6 @@ #include #include #include -#include #include extern PCA9557 expander; @@ -13,8 +12,6 @@ class ThinknodeM5Board : public ESP32Board { public: void begin(); - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); - void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/xiao_c3/XiaoC3Board.h b/variants/xiao_c3/XiaoC3Board.h index 6ea1c15ffc..c5700c6884 100644 --- a/variants/xiao_c3/XiaoC3Board.h +++ b/variants/xiao_c3/XiaoC3Board.h @@ -3,7 +3,6 @@ #include #include -#include #include class XiaoC3Board : public ESP32Board { @@ -40,37 +39,6 @@ class XiaoC3Board : public ESP32Board { #endif } - void enterDeepSleep(uint32_t secs, int8_t wake_pin = -1) { - gpio_set_direction(gpio_num_t(P_LORA_DIO_1), GPIO_MODE_INPUT); - if (wake_pin >= 0) { - gpio_set_direction((gpio_num_t)wake_pin, GPIO_MODE_INPUT); - } - - //hold disable, isolate and power domain config functions may be unnecessary - //gpio_deep_sleep_hold_dis(); - //esp_sleep_config_gpio_isolate(); - gpio_deep_sleep_hold_en(); - -#if defined(LORA_TX_BOOST_PIN) - gpio_hold_en((gpio_num_t) LORA_TX_BOOST_PIN); - gpio_deep_sleep_hold_en(); -#endif - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - if (wake_pin >= 0) { - esp_deep_sleep_enable_gpio_wakeup((1 << P_LORA_DIO_1) | (1 << wake_pin), ESP_GPIO_WAKEUP_GPIO_HIGH); - } else { - esp_deep_sleep_enable_gpio_wakeup(1 << P_LORA_DIO_1, ESP_GPIO_WAKEUP_GPIO_HIGH); - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - #if defined(LORA_TX_BOOST_PIN) || defined(P_LORA_TX_LED) void onBeforeTransmit() override { #if defined(P_LORA_TX_LED) diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index c0e8458d0e..fa70d8a091 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -90,6 +90,11 @@ lib_deps = ${esp32_ota.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Xiao_C3_companion_radio_ble_ps] +extends = env:Xiao_C3_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +board_build.partitions = min_spiffs.csv ; get around 4mb flash limit + [env:Xiao_C3_companion_radio_usb] extends = Xiao_esp32_C3 build_src_filter = ${Xiao_esp32_C3.build_src_filter} diff --git a/variants/xiao_c6/platformio.ini b/variants/xiao_c6/platformio.ini index 9f504b8ed0..0d8c2a79d3 100644 --- a/variants/xiao_c6/platformio.ini +++ b/variants/xiao_c6/platformio.ini @@ -1,6 +1,7 @@ [Xiao_C6] extends = esp32c6_base board = esp32-c6-devkitm-1 +board_build.flash_mode = dio board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_flags = ${esp32c6_base.build_flags} diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index a085433688..f4d1b93e6b 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/xiao_nrf52 -UENV_INCLUDE_GPS -D NRF52_PLATFORM - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D XIAO_NRF52 -D USE_SX1262 -D RADIO_CLASS=CustomSX1262 diff --git a/variants/xiao_s3/platformio.ini b/variants/xiao_s3/platformio.ini index 22464e7d80..b59e69ae43 100644 --- a/variants/xiao_s3/platformio.ini +++ b/variants/xiao_s3/platformio.ini @@ -114,6 +114,10 @@ lib_deps = densaugeo/base64 @ ~1.4.0 adafruit/Adafruit SSD1306 @ ^2.5.13 +[env:Xiao_S3_companion_radio_ble_ps] +extends = env:Xiao_S3_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 + [env:Xiao_S3_companion_radio_usb] extends = Xiao_S3 build_flags = diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index db8c5a9486..41420acf5d 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -173,6 +173,10 @@ lib_deps = densaugeo/base64 @ ~1.4.0 adafruit/Adafruit SSD1306 @ ^2.5.13 +[env:Xiao_S3_WIO_companion_radio_ble_ps] +extends = env:Xiao_S3_WIO_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 + [env:Xiao_S3_WIO_companion_radio_serial] extends = Xiao_S3_WIO build_flags = From 4a733c61cec4e7e92079b221322bf662d8da3008 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Tue, 9 Jun 2026 10:02:03 +0700 Subject: [PATCH 72/94] Renamed to add "femon" env to Heltec v4 and T096 --- variants/heltec_t096/platformio.ini | 6 +++--- variants/heltec_v4/platformio.ini | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index a0e20ef629..5f17436ad6 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -120,7 +120,7 @@ build_src_filter = ${Heltec_t096.build_src_filter} lib_deps = ${Heltec_t096.lib_deps} -[env:Heltec_t096_companion_radio_ble] +[env:Heltec_t096_companion_radio_ble_femon] extends = Heltec_t096 board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 712704 @@ -144,9 +144,9 @@ lib_deps = densaugeo/base64 @ ~1.4.0 [env:Heltec_t096_companion_radio_ble_femoff] -extends = env:Heltec_t096_companion_radio_ble +extends = env:Heltec_t096_companion_radio_ble_femon build_flags = - ${env:Heltec_t096_companion_radio_ble.build_flags} + ${env:Heltec_t096_companion_radio_ble_femon.build_flags} -D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off [env:Heltec_t096_companion_radio_usb] diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index 5ad2b1e979..caa71a33f7 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -222,14 +222,14 @@ lib_deps = ${heltec_v4_oled.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:heltec_v4_companion_radio_ble_ps] +[env:heltec_v4_companion_radio_ble_ps_femon] extends = env:heltec_v4_companion_radio_ble platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 [env:heltec_v4_3_companion_radio_ble_ps_femoff] -extends = env:heltec_v4_companion_radio_ble_ps +extends = env:heltec_v4_companion_radio_ble_ps_femon build_flags = - ${env:heltec_v4_companion_radio_ble_ps.build_flags} + ${env:heltec_v4_companion_radio_ble_ps_femon.build_flags} -D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off [env:heltec_v4_companion_radio_wifi] From e0733f834699e78f9b93cec853a20f53b26b9175 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Tue, 9 Jun 2026 11:34:36 +0700 Subject: [PATCH 73/94] Added 0x77 for ENV_INCLUDE_BME680_BSEC --- src/helpers/sensors/EnvironmentSensorManager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 637d61afae..ea57677f25 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -15,6 +15,7 @@ #if ENV_INCLUDE_BME680_BSEC #ifndef TELEM_BME680_ADDRESS #define TELEM_BME680_ADDRESS 0x76 +#define TELEM_BME680_ADDRESS_2 0x77 #endif #define TELEM_BME680_SEALEVELPRESSURE_HPA (1013.25) #include From 7c1b1c6e5e64da4d494881afebbe545faaf62f0a Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Tue, 9 Jun 2026 11:59:14 +0700 Subject: [PATCH 74/94] Hotfix: CMD_SEND_RAW_PACKET not freeing packet on parse failure. https://github.com/meshcore-dev/MeshCore/pull/2722 --- examples/companion_radio/MyMesh.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 6bf2671a3e..1dd5162bf9 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2010,6 +2010,7 @@ void MyMesh::handleCmdFrame(size_t len) { sendPacket(pkt, priority, 0); writeOKFrame(); } else { + releasePacket(pkt); writeErrFrame(ERR_CODE_ILLEGAL_ARG); } } else { From c94ed29ca36e73d9026319af18b8977648ec8809 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 12 Jun 2026 03:18:10 +1200 Subject: [PATCH 75/94] add github workflow to close stale issues --- .github/workflows/stale-bot.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/stale-bot.yml diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml new file mode 100644 index 0000000000..afe874f801 --- /dev/null +++ b/.github/workflows/stale-bot.yml @@ -0,0 +1,32 @@ +name: 'Run Stale Bot' +on: + schedule: + - cron: '30 1 * * *' # daily at 1:30am + workflow_dispatch: {} + +permissions: + actions: write + issues: write + pull-requests: write + +jobs: + close-issues: + # only run on main repo, not forks + if: github.repository == "meshcore-dev/MeshCore" + runs-on: ubuntu-latest + steps: + - name: Close Stale Issues + uses: actions/stale@v10 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + # auto close issues + days-before-issue-stale: 60 + days-before-issue-close: 7 + exempt-issue-labels: "keep-open" + stale-issue-label: "stale" + stale-issue-message: "This issue is stale because it has been open for 60 days with no activity. Remove the stale label or add a comment if this issue is still relevant, otherwise this issue will automatically close in 7 days." + close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale." + # don't auto close prs + days-before-pr-stale: -1 + days-before-pr-close: -1 + \ No newline at end of file From 3b3992539945b861801ab709b2361cb03edee657 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 12 Jun 2026 03:20:44 +1200 Subject: [PATCH 76/94] use single quotes for repo name --- .github/workflows/stale-bot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index afe874f801..ec16658756 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -12,7 +12,7 @@ permissions: jobs: close-issues: # only run on main repo, not forks - if: github.repository == "meshcore-dev/MeshCore" + if: github.repository == 'meshcore-dev/MeshCore' runs-on: ubuntu-latest steps: - name: Close Stale Issues From 06130dce29c907ebc014dc52156fbb1e52210903 Mon Sep 17 00:00:00 2001 From: formtapez Date: Fri, 12 Jun 2026 12:11:12 +0200 Subject: [PATCH 77/94] added some missing CLI commands --- docs/cli_commands.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 9accb2998d..f482cfcb68 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -28,12 +28,25 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Usage:** - `reboot` +**Note:** No reply is sent. + +--- + +### Power-off the node +**Usage:** +- `poweroff`, or +- `shutdown +` +**Note:** No reply is sent. + --- ### Reset the clock and reboot **Usage:** - `clkreboot` +**Note:** No reply is sent. + --- ### Sync the clock with the remote device @@ -632,10 +645,21 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `value`: Maximum flood hop count (0-64) for a packet without a scope (no region set) -**Default:** `0xFF` - indicates it hasn't been set, will track flood.max until it is. +**Default:** `64` - indicates it hasn't been set, will track flood.max until it is. **Note:** An alternative to `region denyf *`, setting `flood.max.unscoped` to a lower value such as `3` would allow for local unscoped messages to propagate, while preventing noisy neighbors from flooding a local region. +--- + +#### Limit the number of hops for an advert flood message +**Usage:** +- `get flood.max.advert` +- `set flood.max.advert ` + +**Parameters:** +- `value`: Maximum flood hop count (0-64) for an advert packet + +**Default:** `8` --- From d3444e6b0be513f982f3bcb7d2f05a091f16a2fd Mon Sep 17 00:00:00 2001 From: formtapez Date: Fri, 12 Jun 2026 12:14:46 +0200 Subject: [PATCH 78/94] fix formatting --- docs/cli_commands.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index f482cfcb68..66a9b77afe 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -35,8 +35,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore ### Power-off the node **Usage:** - `poweroff`, or -- `shutdown -` +- `shutdown` + **Note:** No reply is sent. --- @@ -645,7 +645,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `value`: Maximum flood hop count (0-64) for a packet without a scope (no region set) -**Default:** `64` - indicates it hasn't been set, will track flood.max until it is. +**Default:** `64` - (`0xFF` indicates it hasn't been set, will track flood.max until it is.) **Note:** An alternative to `region denyf *`, setting `flood.max.unscoped` to a lower value such as `3` would allow for local unscoped messages to propagate, while preventing noisy neighbors from flooding a local region. From 19efda9f2e52e9da10c44bf55952ae72751d09cf Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 12 Jun 2026 21:19:05 +0700 Subject: [PATCH 79/94] Reverted code change to GPS --- src/helpers/sensors/MicroNMEALocationProvider.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index 8b3c58679a..eec466d3aa 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -76,7 +76,6 @@ public : void begin() override { claim(); if (_pin_en != -1) { - pinMode(_pin_en, OUTPUT); digitalWrite(_pin_en, PIN_GPS_EN_ACTIVE); } if (_pin_reset != -1) { @@ -95,7 +94,6 @@ public : void stop() override { if (_pin_en != -1) { digitalWrite(_pin_en, !PIN_GPS_EN_ACTIVE); - pinMode(_pin_en, INPUT); // Reduce 0.3mA leaking } if (_pin_reset != -1) { digitalWrite(_pin_reset, GPS_RESET_FORCE); From 29dc93b184ae2ce7a2926b1e3c1b5e45c985976c Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 15:56:13 -0700 Subject: [PATCH 80/94] Document Halo direct retry settings only --- docs/halo_keymind_settings.md | 361 ---------------------------------- docs/halo_settings.md | 136 +++++++++++++ 2 files changed, 136 insertions(+), 361 deletions(-) delete mode 100644 docs/halo_keymind_settings.md create mode 100644 docs/halo_settings.md diff --git a/docs/halo_keymind_settings.md b/docs/halo_keymind_settings.md deleted file mode 100644 index ca2c080329..0000000000 --- a/docs/halo_keymind_settings.md +++ /dev/null @@ -1,361 +0,0 @@ -# Halo and Keymind Branch Settings - -This file covers only CLI settings and helper commands added by the Halo or -Keymind branches. Use `docs/cli_commands.md` for the general MeshCore CLI. - -## Quick Start - -```text -set retry.preset rooftop -set direct.retry.heard on -set flood.retry.advert off -set flood.retry.bridge off -set flood.retry.prefixes none -set flood.retry.ignore none -``` - -Then verify: - -```text -get retry.preset -get direct.retry.heard -get flood.retry.advert -get flood.retry.prefixes -get flood.retry.ignore -``` - -Use prefixes from the analyzer or neighbors list or `get recent.repeater` after the repeater has been online for a few hours. - -## Common Examples - -Disable retrying advert packets: - -```text -set flood.retry.advert off -get flood.retry.advert -``` - -Ignore a repeater as a successful flood retry echo: -Use this if you have a car repeater and a house repeater; have the house ignore the car. - -```text -set flood.retry.ignore 71CE82,C7618C -get flood.retry.ignore -``` - -Only accept specific downstream relays as flood retry success: -You're in a hole and need to hit a mountain top repeater to get out; keep trying till one you see one of these send out your packet. - -```text -set flood.retry.prefixes A58296,860CCA,425E5C -get flood.retry.prefixes -``` - -Bridge two groups of repeaters: - -```text -set flood.retry.bridge on -set flood.retry.bucket 1 71CE82,C7618C -set flood.retry.bucket 2 BEEBB0,425E5C -get flood.retry.bucket.1 -get flood.retry.bucket.2 -``` - -Return to simple non-bridge flood retry: - -```text -set flood.retry.bridge off -set flood.retry.prefixes none -set flood.retry.ignore none -``` - -## Added Settings - -| Setting | What it does | How to use | Example | -| --- | --- | --- | --- | -| `battery.alert` | Sends opt-in low-battery warnings to `#repeaters`. | `get battery.alert`, `set battery.alert on/off` | `set battery.alert on` | -| `battery.alert.low` | Warning threshold percentage. Must be greater than `battery.alert.critical`. | `get battery.alert.low`, `set battery.alert.low <1-100>` | `set battery.alert.low 20` | -| `battery.alert.critical` | Critical threshold percentage. Critical warnings repeat more often. | `get battery.alert.critical`, `set battery.alert.critical <0-99>` | `set battery.alert.critical 10` | -| `recent.repeater` | Shows, seeds, or clears the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. | `get recent.repeater`, `get recent.repeater `, `set recent.repeater `, `clear recent.repeater` | `set recent.repeater A1B2C3 -8.5` | -| `outpath` | Overrides the primary direct route used for replies to the current remote client. | `get outpath`, `set outpath `, `set outpath direct`, `set outpath clear`, `set outpath flood` | `set outpath A1B2C3,D4E5F6` | -| `altpath` | Optional second direct route used for duplicate response attempts to the current remote client. | `get altpath`, `set altpath `, `set altpath clear` | `set altpath A1B2C3,D4E5F6` | - -## Other Keymind Commands - -| Command | What it does | How to use | Example | -| --- | --- | --- | --- | -| `send text.flood` | Sends a `#repeaters` flood text message formatted as `: `. | `send text.flood ` | `send text.flood checking ridge link` | - -## Battery Alerts - -Battery alerts are off by default. When enabled, the repeater checks once per -minute and sends a flood text warning to `#repeaters` when voltage is above -`1 V` and the estimated battery percent is below `battery.alert.low`. - -Warnings repeat every `24` hours, or every `12` hours when the estimate is -below `battery.alert.critical`. - -Defaults: - -| Setting | Default | -| --- | ---: | -| `battery.alert` | `off` | -| `battery.alert.low` | `20` | -| `battery.alert.critical` | `10` | - -Example: - -```text -set battery.alert.low 20 -set battery.alert.critical 10 -set battery.alert on -get battery.alert -``` - -## Recent Repeater Table - -Direct retry uses the recent repeater table when `direct.retry.heard` is `on`. -Bridge buckets also use this table: a configured bucket prefix is active only -when it was heard within the last hour. - -Show learned rows: - -```text -get recent.repeater -get recent.repeater 2 -get recent.repeater page 3 -``` - -Seed or correct a prefix: - -```text -set recent.repeater A1B2C3 8.5 -``` - -Clear learned and manually seeded rows: - -```text -clear recent.repeater -``` - -Rows are sorted by prefix width, then SNR. A full direct retry failure lowers -the matching row by `0.25 dB`. - -Serial CLI pages contain up to `128` rows. Remote LoRa CLI pages contain up to -`7` rows. - -## Direct Path Overrides - -`outpath` and `altpath` apply to the current remote client ACL entry. They need -remote client context, so they are not useful from the local serial CLI. - -Set paths with comma-separated hop hashes. Each hop must be `2`, `4`, or `6` -hex characters, and all hops in one path must use the same width. - -```text -get outpath -set outpath A1B2C3,D4E5F6 -set outpath direct -set outpath clear -set outpath flood - -get altpath -set altpath A1B2C3,D4E5F6 -set altpath clear -``` - -`set outpath direct` sets a zero-hop direct route for a client reachable without -repeaters. `set outpath clear` forgets the override and lets normal path -discovery fill it again. `set outpath flood` forces replies to use flood packets -until the client logs in again. `altpath` sends a duplicate reply over a second -direct route; clearing it returns replies to a single route. - -## Direct Retry Settings - -Direct retry applies to direct-routed packets. A queued resend is canceled when the next-hop echo is heard. - -| Setting | What it does | How to use | Example | -| --- | --- | --- | --- | -| `retry.preset` | Applies shared direct and flood retry defaults. Values: `infra`, `rooftop`, `mobile` or `0`, `1`, `2`. | `get retry.preset`, `set retry.preset ` | `set retry.preset rooftop` | -| `direct.retry.heard` | Uses the recent repeater table as the direct retry eligibility gate. | `get direct.retry.heard`, `set direct.retry.heard on/off` | `set direct.retry.heard on` | -| `direct.retry.margin` | SNR margin in dB above the SF-specific receive floor. | `get direct.retry.margin`, `set direct.retry.margin <0-40>` | `set direct.retry.margin 5` | -| `direct.retry.count` | Maximum direct retry attempts after initial TX. | `get direct.retry.count`, `set direct.retry.count <1-15>` | `set direct.retry.count 15` | -| `direct.retry.base` | Base wait in milliseconds before retry. | `get direct.retry.base`, `set direct.retry.base <10-5000>` | `set direct.retry.base 175` | -| `direct.retry.step` | Milliseconds added per retry attempt. | `get direct.retry.step`, `set direct.retry.step <0-5000>` | `set direct.retry.step 100` | -| `direct.retry.cr` | Adaptive coding-rate thresholds for direct retry packets. Uses `CR4`, `CR5`, `CR7`, or `CR8`; `CR6` is never selected. | `get direct.retry.cr`, `set direct.retry.cr ,,,`, `set direct.retry.cr off` | `set direct.retry.cr 10.0,7.5,2.5,0` | - -The default adaptive coding-rate profile is `10.0,7.5,2.5,2.5`. -SNR `10.0 dB` and up uses `CR4`, `7.5 dB` and up uses `CR5`, -`2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. If no -recent repeater table entry is available, retry packets use `CR5`. Use -`set direct.retry.cr off` to disable adaptive coding-rate overrides. If -adaptive selection chooses `CR4`, retries after the third attempt use -`CR5`. - -Preset details: - -| Preset | Base | Count | Step | SNR gate | -| --- | ---: | ---: | ---: | --- | -| `infra` | `275 ms` | `4` | `150 ms` | SF floor + `15 dB` | -| `rooftop` | `175 ms` | `15` | `100 ms` | SF floor + `5 dB` | -| `mobile` | `175 ms` | `15` | `50 ms` | SF floor | - -Example for a quiet fixed repeater: - -```text -set retry.preset rooftop -set direct.retry.heard on -set direct.retry.margin 5 -``` - -Example for a moving or weak-link node: - -```text -set retry.preset mobile -set direct.retry.margin 0 -``` - -## Flood And Advert Settings - -Flood retry applies to flood-routed packets. A queued retry is canceled when a qualifying downstream echo is heard. - -| Setting | What it does | How to use | Example | -| --- | --- | --- | --- | -| `flood.retry.count` | Maximum flood retry attempts after initial TX. `0` disables flood retry. | `get flood.retry.count`, `set flood.retry.count <0-15>` | `set flood.retry.count 7` | -| `flood.retry.path` | Maximum path hash count eligible for flood retry, or `off` to disable the gate. | `get flood.retry.path`, `set flood.retry.path <0-63/off>` | `set flood.retry.path 1` | -| `flood.retry.advert` | Allows or blocks retry for node advert packets (`type=4`). Default is `off`. | `get flood.retry.advert`, `set flood.retry.advert on/off` | `set flood.retry.advert off` | -| `flood.retry.prefixes` | Target prefixes. If set, only matching downstream echoes cancel a retry. | `get flood.retry.prefixes`, `set flood.retry.prefixes ` | `set flood.retry.prefixes BEEBB0,425E5C` | -| `flood.retry.ignore` | Ignored prefixes. In non-bridge retry, ignored last-hop echoes do not cancel retry. | `get flood.retry.ignore`, `set flood.retry.ignore ` | `set flood.retry.ignore 71CE82,C7618C` | -| `flood.retry.bridge` | Enables bucket-based bridge retry logic. | `get flood.retry.bridge`, `set flood.retry.bridge on/off` | `set flood.retry.bridge on` | -| `flood.retry.bucket.` | Shows one bridge bucket. Buckets are numbered `1`-`6`. | `get flood.retry.bucket.` | `get flood.retry.bucket.1` | -| `flood.retry.bucket` | Sets bridge bucket prefixes. | `set flood.retry.bucket <1-6> ` | `set flood.retry.bucket 1 71CE82,C7618C` | - -The shared retry preset sets these flood defaults: - -| Preset | Retry count | Path gate | -| --- | ---: | ---: | -| `infra` | `1` | `1` | -| `rooftop` | `3` | `2` | -| `mobile` | `3` | `1` | - -Example for path-gated retry: - -```text -set retry.preset rooftop -set flood.retry.path 1 -set flood.retry.advert off -set flood.retry.ignore 71CE82,C7618C -``` - -## North South Buckets - -Buckets describe groups of repeaters on different sides of this relay. Bucket -numbers do not have built-in meanings; this example uses bucket `1` for North -and bucket `2` for South. - -```text - North bucket 1 - +-----------------------+ - | A1B2C3 D4E5F6 | - | North A North B | - +-----------+-----------+ - | - v - +-----------+ - | This node | - +-----------+ - ^ - | - +-----------+-----------+ - | 71CE82 C7618C | - | South A South B | - +-----------------------+ - South bucket 2 -``` - -Configure the buckets: - -```text -set flood.retry.bridge on -set flood.retry.bucket 1 A1B2C3,D4E5F6 -set flood.retry.bucket 2 71CE82,C7618C -set flood.retry.ignore none -``` - -Packet heard from the North: - -```text - heard source - | - v - +--------------+ retry targets - | North bucket | -----> South bucket - | bucket 1 | -----> Other fresh/unbucketed relays - +--------------+ -``` - -Packet heard from the South: - -```text - heard source - | - v - +--------------+ retry targets - | South bucket | -----> North bucket - | bucket 2 | -----> Other fresh/unbucketed relays - +--------------+ -``` - -Packet heard from an unbucketed or pathless source: - -```text - heard source - | - v - +--------------+ retry targets - | Other bucket | -----> North bucket - | implicit | -----> South bucket - +--------------+ -``` - -Bridge retry stays eligible until every target bucket has been heard or -`flood.retry.count` is exhausted. A configured bucket is a target only when at -least one of its prefixes is fresh in `recent.repeater`. Prefixes in -`flood.retry.ignore` never count as bucket hits. - -## Troubleshooting - -If advert packets are still retrying: - -```text -get flood.retry.advert -set flood.retry.advert off -``` - -If ignored prefixes still appear in `flood retry good` logs: - -```text -get flood.retry.ignore -set flood.retry.ignore -``` - -The ignored prefix must match the last hop shown as `heard=`. For example, this log needs `C7618C` in the ignore list: - -```text -flood retry good (... path=7773D0>C7618C, heard=C7618C ...) -``` - -If retries are too aggressive: - -```text -set flood.retry.count 1 -set flood.retry.path 1 -set direct.retry.count 4 -``` - -If retries are too sparse: - -```text -set flood.retry.count 7 -set flood.retry.path 2 -``` diff --git a/docs/halo_settings.md b/docs/halo_settings.md new file mode 100644 index 0000000000..9324487d29 --- /dev/null +++ b/docs/halo_settings.md @@ -0,0 +1,136 @@ +# Halo Direct Message Retry Settings + +This file covers only CLI settings and helper commands added for Halo direct-message retry behavior. Use `docs/cli_commands.md` for the general MeshCore CLI. + +Halo retry applies to direct-routed packets. A queued resend is canceled when the next-hop echo is heard. + +## Quick Start + +```text +set retry.preset rooftop +set direct.retry.heard on +get retry.preset +get direct.retry.heard +get direct.retry.count +get direct.retry.base +get direct.retry.step +``` + +Use prefixes from the analyzer, neighbors list, or `get recent.repeater` after the repeater has been online for a few hours. + +## Added Halo Settings + +| Setting | What it does | How to use | Example | +| --- | --- | --- | --- | +| `recent.repeater` | Shows, seeds, or clears the recent repeater prefix/SNR table used by direct retry. | `get recent.repeater`, `get recent.repeater `, `set recent.repeater `, `clear recent.repeater` | `set recent.repeater A1B2C3 -8.5` | +| `outpath` | Overrides the primary direct route used for replies to the current remote client. | `get outpath`, `set outpath `, `set outpath direct`, `set outpath clear`, `set outpath flood` | `set outpath A1B2C3,D4E5F6` | +| `altpath` | Optional second direct route used for duplicate response attempts to the current remote client. | `get altpath`, `set altpath `, `set altpath clear` | `set altpath A1B2C3,D4E5F6` | +| `retry.preset` | Applies direct retry defaults. Values: `infra`, `rooftop`, `mobile` or `0`, `1`, `2`. | `get retry.preset`, `set retry.preset ` | `set retry.preset rooftop` | +| `direct.retry.heard` | Uses the recent repeater table as the direct retry eligibility gate. | `get direct.retry.heard`, `set direct.retry.heard on/off` | `set direct.retry.heard on` | +| `direct.retry.margin` | SNR margin in dB above the SF-specific receive floor. | `get direct.retry.margin`, `set direct.retry.margin <0-40>` | `set direct.retry.margin 5` | +| `direct.retry.count` | Maximum direct retry attempts after initial TX. | `get direct.retry.count`, `set direct.retry.count <1-15>` | `set direct.retry.count 15` | +| `direct.retry.base` | Base wait in milliseconds before retry. | `get direct.retry.base`, `set direct.retry.base <10-5000>` | `set direct.retry.base 175` | +| `direct.retry.step` | Milliseconds added per retry attempt. | `get direct.retry.step`, `set direct.retry.step <0-5000>` | `set direct.retry.step 100` | +| `direct.retry.cr` | Adaptive coding-rate thresholds for direct retry packets. Uses `CR4`, `CR5`, `CR7`, or `CR8`; `CR6` is never selected. | `get direct.retry.cr`, `set direct.retry.cr ,,,`, `set direct.retry.cr off` | `set direct.retry.cr 10.0,7.5,2.5,0` | + +## Recent Repeater Table + +Direct retry uses the recent repeater table when `direct.retry.heard` is `on`. + +Show learned rows: + +```text +get recent.repeater +get recent.repeater 2 +get recent.repeater page 3 +``` + +Seed or correct a prefix: + +```text +set recent.repeater A1B2C3 8.5 +``` + +Clear learned and manually seeded rows: + +```text +clear recent.repeater +``` + +Rows are sorted by prefix width, then SNR. A full direct retry failure lowers the matching row by `0.25 dB`. + +Serial CLI pages contain up to `128` rows. Remote LoRa CLI pages contain up to `7` rows. + +## Direct Path Overrides + +`outpath` and `altpath` apply to the current remote client ACL entry. They need remote client context, so they are not useful from the local serial CLI. + +Set paths with comma-separated hop hashes. Each hop must be `2`, `4`, or `6` hex characters, and all hops in one path must use the same width. + +```text +get outpath +set outpath A1B2C3,D4E5F6 +set outpath direct +set outpath clear +set outpath flood + +get altpath +set altpath A1B2C3,D4E5F6 +set altpath clear +``` + +`set outpath direct` sets a zero-hop direct route for a client reachable without repeaters. `set outpath clear` forgets the override and lets normal path discovery fill it again. `set outpath flood` forces replies to use flood packets until the client logs in again. `altpath` sends a duplicate reply over a second direct route; clearing it returns replies to a single route. + +## Direct Retry Details + +The default adaptive coding-rate profile is `10.0,7.5,2.5,2.5`. SNR `10.0 dB` and up uses `CR4`, `7.5 dB` and up uses `CR5`, `2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. If no recent repeater table entry is available, retry packets use `CR5`. + +Use `set direct.retry.cr off` to disable adaptive coding-rate overrides. If adaptive selection chooses `CR4`, retries after the third attempt use `CR5`. + +Preset details: + +| Preset | Base | Count | Step | SNR gate | +| --- | ---: | ---: | ---: | --- | +| `infra` | `275 ms` | `4` | `150 ms` | SF floor + `15 dB` | +| `rooftop` | `175 ms` | `15` | `100 ms` | SF floor + `5 dB` | +| `mobile` | `175 ms` | `15` | `50 ms` | SF floor | + +Example for a quiet fixed repeater: + +```text +set retry.preset rooftop +set direct.retry.heard on +set direct.retry.margin 5 +``` + +Example for a moving or weak-link node: + +```text +set retry.preset mobile +set direct.retry.margin 0 +``` + +## Troubleshooting + +If direct retries are too aggressive: + +```text +set direct.retry.count 4 +set direct.retry.margin 10 +``` + +If direct retries are too sparse: + +```text +set direct.retry.count 15 +set direct.retry.margin 0 +``` + +If direct retry is skipping a path you expect it to retry: + +```text +get direct.retry.heard +get recent.repeater +``` + +Either disable the heard gate with `set direct.retry.heard off`, or seed the next-hop prefix with `set recent.repeater `. From 17dc51dbcea17c7d210b5819b24db7fb92af414b Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 15:58:37 -0700 Subject: [PATCH 81/94] Remove build and variant changes from Halo retry branch --- boards/nrf52840_s140_v6.ld | 15 +----- boards/nrf52840_s140_v6_extrafs.ld | 15 +----- boards/nrf52840_s140_v7.ld | 15 +----- boards/nrf52840_s140_v7_extrafs.ld | 12 +---- build.sh | 7 ++- variants/gat562_30s_mesh_kit/platformio.ini | 3 +- variants/gat562_mesh_evb_pro/platformio.ini | 2 +- .../gat562_mesh_tracker_pro/platformio.ini | 3 +- variants/gat562_mesh_watch13/platformio.ini | 2 +- variants/heltec_ct62/platformio.ini | 4 -- variants/heltec_e213/HeltecE213Board.cpp | 27 ++++++++++ variants/heltec_e213/HeltecE213Board.h | 3 ++ variants/heltec_e290/HeltecE290Board.cpp | 27 ++++++++++ variants/heltec_e290/HeltecE290Board.h | 3 ++ variants/heltec_t096/LoRaFEMControl.h | 2 +- variants/heltec_t096/platformio.ini | 10 +--- variants/heltec_t114/platformio.ini | 2 +- variants/heltec_t190/HeltecT190Board.cpp | 27 ++++++++++ variants/heltec_t190/HeltecT190Board.h | 3 ++ variants/heltec_tracker/platformio.ini | 4 -- .../HeltecTrackerV2Board.cpp | 31 +++++++++-- .../heltec_tracker_v2/HeltecTrackerV2Board.h | 2 + variants/heltec_tracker_v2/LoRaFEMControl.h | 2 +- variants/heltec_tracker_v2/platformio.ini | 4 -- variants/heltec_v2/HeltecV2Board.h | 25 +++++++++ variants/heltec_v2/platformio.ini | 4 -- variants/heltec_v3/HeltecV3Board.h | 29 +++++++++++ variants/heltec_v3/platformio.ini | 8 --- variants/heltec_v4/HeltecV4Board.h | 2 + variants/heltec_v4/LoRaFEMControl.h | 2 +- variants/heltec_v4/platformio.ini | 18 ------- variants/heltec_wireless_paper/platformio.ini | 4 -- variants/lilygo_t3s3/platformio.ini | 4 -- variants/lilygo_tbeam_1w/platformio.ini | 5 -- variants/lilygo_tbeam_SX1262/platformio.ini | 7 --- variants/lilygo_tbeam_SX1276/platformio.ini | 4 -- .../platformio.ini | 4 -- variants/lilygo_tdeck/TDeckBoard.h | 24 +++++++++ variants/lilygo_tlora_v2_1/platformio.ini | 6 +-- variants/promicro/platformio.ini | 1 - variants/rak3112/RAK3112Board.h | 29 +++++++++++ variants/rak3401/platformio.ini | 3 +- variants/rak4631/platformio.ini | 3 +- variants/rak_wismesh_tag/platformio.ini | 1 - variants/sensecap_solar/platformio.ini | 2 +- variants/station_g2/StationG2Board.h | 24 +++++++++ variants/station_g2/platformio.ini | 5 ++ variants/station_g2/target.cpp | 15 ------ variants/station_g2/target.h | 3 -- variants/thinknode_m2/ThinknodeM2Board.cpp | 52 +++++++++++-------- variants/thinknode_m2/ThinknodeM2Board.h | 4 ++ variants/thinknode_m5/ThinknodeM5Board.cpp | 8 +++ variants/thinknode_m5/ThinknodeM5Board.h | 3 ++ variants/xiao_c3/XiaoC3Board.h | 32 ++++++++++++ variants/xiao_c3/platformio.ini | 5 -- variants/xiao_c6/platformio.ini | 1 - variants/xiao_nrf52/platformio.ini | 2 +- variants/xiao_s3/platformio.ini | 4 -- variants/xiao_s3_wio/platformio.ini | 4 -- 59 files changed, 360 insertions(+), 212 deletions(-) diff --git a/boards/nrf52840_s140_v6.ld b/boards/nrf52840_s140_v6.ld index d0c7d1dc8b..6dad975b0d 100644 --- a/boards/nrf52840_s140_v6.ld +++ b/boards/nrf52840_s140_v6.ld @@ -7,9 +7,6 @@ MEMORY { FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xED000 - 0x26000 - /* To keep data in RAM across resets */ - PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8 - /* SRAM required by Softdevice depend on * - Attribute Table Size (Number of Services and Characteristics) * - Vendor UUID count @@ -17,19 +14,11 @@ MEMORY * - Concurrent connection peripheral + central + secure links * - Event Len, HVN queue, Write CMD queue */ - RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8 + RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 } SECTIONS { - . = ALIGN(4); - .persistent (NOLOAD) : - { - KEEP(*(.persistent_magic)) - KEEP(*(.persistent_data)) - . = ALIGN(4); - } > PERSISTENT_RAM - . = ALIGN(4); .svc_data : { @@ -44,6 +33,6 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM -} +} INSERT AFTER .data; INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v6_extrafs.ld b/boards/nrf52840_s140_v6_extrafs.ld index bd4547473a..352610679e 100644 --- a/boards/nrf52840_s140_v6_extrafs.ld +++ b/boards/nrf52840_s140_v6_extrafs.ld @@ -7,9 +7,6 @@ MEMORY { FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xD4000 - 0x26000 - /* To keep data in RAM across resets */ - PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8 - /* SRAM required by Softdevice depend on * - Attribute Table Size (Number of Services and Characteristics) * - Vendor UUID count @@ -17,19 +14,11 @@ MEMORY * - Concurrent connection peripheral + central + secure links * - Event Len, HVN queue, Write CMD queue */ - RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8 + RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 } SECTIONS { - . = ALIGN(4); - .persistent (NOLOAD) : - { - KEEP(*(.persistent_magic)) - KEEP(*(.persistent_data)) - . = ALIGN(4); - } > PERSISTENT_RAM - . = ALIGN(4); .svc_data : { @@ -44,6 +33,6 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM -} +} INSERT AFTER .data; INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v7.ld b/boards/nrf52840_s140_v7.ld index 2333238f28..6aaeb4034f 100644 --- a/boards/nrf52840_s140_v7.ld +++ b/boards/nrf52840_s140_v7.ld @@ -7,9 +7,6 @@ MEMORY { FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xED000 - 0x27000 - /* To keep data in RAM across resets */ - PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8 - /* SRAM required by Softdevice depend on * - Attribute Table Size (Number of Services and Characteristics) * - Vendor UUID count @@ -17,19 +14,11 @@ MEMORY * - Concurrent connection peripheral + central + secure links * - Event Len, HVN queue, Write CMD queue */ - RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8 + RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 } SECTIONS { - . = ALIGN(4); - .persistent (NOLOAD) : - { - KEEP(*(.persistent_magic)) - KEEP(*(.persistent_data)) - . = ALIGN(4); - } > PERSISTENT_RAM - . = ALIGN(4); .svc_data : { @@ -44,6 +33,6 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM -} +} INSERT AFTER .data; INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v7_extrafs.ld b/boards/nrf52840_s140_v7_extrafs.ld index 48348188af..5956183aa3 100644 --- a/boards/nrf52840_s140_v7_extrafs.ld +++ b/boards/nrf52840_s140_v7_extrafs.ld @@ -14,19 +14,11 @@ MEMORY * - Concurrent connection peripheral + central + secure links * - Event Len, HVN queue, Write CMD queue */ - RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8 + RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 } SECTIONS { - . = ALIGN(4); - .persistent (NOLOAD) : - { - KEEP(*(.persistent_magic)) - KEEP(*(.persistent_data)) - . = ALIGN(4); - } > PERSISTENT_RAM - . = ALIGN(4); .svc_data : { @@ -41,6 +33,6 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM -} +} INSERT AFTER .data; INCLUDE "nrf52_common.ld" diff --git a/build.sh b/build.sh index 006eae9698..313c4c47a0 100755 --- a/build.sh +++ b/build.sh @@ -134,8 +134,7 @@ build_firmware() { # set firmware version string # e.g: v1.0.0-abcdef - # FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}-${COMMIT_HASH}" - FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}" + FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}-${COMMIT_HASH}" # craft filename # e.g: RAK_4631_Repeater-v1.0.0-SHA @@ -153,8 +152,8 @@ build_firmware() { # build merge-bin for esp32 fresh install, copy .bins to out folder (e.g: Heltec_v3_room_server-v1.0.0-SHA.bin) if [ "$ENV_PLATFORM" == "ESP32_PLATFORM" ]; then pio run -t mergebin -e $1 - cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}-upgrade.bin 2>/dev/null || true - cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-freshInstall-merged.bin 2>/dev/null || true + cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true + cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-merged.bin 2>/dev/null || true fi # build .uf2 for nrf52 boards, copy .uf2 and .zip to out folder (e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2) diff --git a/variants/gat562_30s_mesh_kit/platformio.ini b/variants/gat562_30s_mesh_kit/platformio.ini index aa3915a4cd..2baac2561b 100644 --- a/variants/gat562_30s_mesh_kit/platformio.ini +++ b/variants/gat562_30s_mesh_kit/platformio.ini @@ -2,13 +2,12 @@ extends = nrf52_base board = rak4631 board_check = true -board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_30s_mesh_kit -D RAK_4631 -D RAK_BOARD -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/gat562_mesh_evb_pro/platformio.ini b/variants/gat562_mesh_evb_pro/platformio.ini index d7de585ad3..b3e894174a 100644 --- a/variants/gat562_mesh_evb_pro/platformio.ini +++ b/variants/gat562_mesh_evb_pro/platformio.ini @@ -5,7 +5,7 @@ board_check = true build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_evb_pro -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D RADIO_CLASS=CustomSX1262 diff --git a/variants/gat562_mesh_tracker_pro/platformio.ini b/variants/gat562_mesh_tracker_pro/platformio.ini index 78ec7d0189..af153b8fc2 100644 --- a/variants/gat562_mesh_tracker_pro/platformio.ini +++ b/variants/gat562_mesh_tracker_pro/platformio.ini @@ -2,11 +2,10 @@ extends = nrf52_base board = rak4631 board_check = true -board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_tracker_pro -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/gat562_mesh_watch13/platformio.ini b/variants/gat562_mesh_watch13/platformio.ini index f457424f10..f3510b74aa 100644 --- a/variants/gat562_mesh_watch13/platformio.ini +++ b/variants/gat562_mesh_watch13/platformio.ini @@ -8,7 +8,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/gat562_mesh_watch13 -D RAK_4631 -D RAK_BOARD -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/heltec_ct62/platformio.ini b/variants/heltec_ct62/platformio.ini index e8becc7e55..0179d9658c 100644 --- a/variants/heltec_ct62/platformio.ini +++ b/variants/heltec_ct62/platformio.ini @@ -130,10 +130,6 @@ lib_deps = ${esp32_ota.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Heltec_ct62_companion_radio_ble_ps] -extends = env:Heltec_ct62_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Heltec_ct62_sensor] extends = Heltec_ct62 build_flags = diff --git a/variants/heltec_e213/HeltecE213Board.cpp b/variants/heltec_e213/HeltecE213Board.cpp index 88737c4d53..af11531821 100644 --- a/variants/heltec_e213/HeltecE213Board.cpp +++ b/variants/heltec_e213/HeltecE213Board.cpp @@ -20,6 +20,33 @@ void HeltecE213Board::begin() { } } + void HeltecE213Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + + void HeltecE213Board::powerOff() { + enterDeepSleep(0); + } + uint16_t HeltecE213Board::getBattMilliVolts() { analogReadResolution(10); digitalWrite(PIN_ADC_CTRL, HIGH); diff --git a/variants/heltec_e213/HeltecE213Board.h b/variants/heltec_e213/HeltecE213Board.h index fadc038f94..2192c14104 100644 --- a/variants/heltec_e213/HeltecE213Board.h +++ b/variants/heltec_e213/HeltecE213Board.h @@ -3,6 +3,7 @@ #include #include #include +#include class HeltecE213Board : public ESP32Board { @@ -12,6 +13,8 @@ class HeltecE213Board : public ESP32Board { HeltecE213Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { } void begin(); + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); + void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; }; diff --git a/variants/heltec_e290/HeltecE290Board.cpp b/variants/heltec_e290/HeltecE290Board.cpp index 96ec59c9bd..3994a20616 100644 --- a/variants/heltec_e290/HeltecE290Board.cpp +++ b/variants/heltec_e290/HeltecE290Board.cpp @@ -20,6 +20,33 @@ void HeltecE290Board::begin() { } } + void HeltecE290Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + + void HeltecE290Board::powerOff() { + enterDeepSleep(0); + } + uint16_t HeltecE290Board::getBattMilliVolts() { analogReadResolution(10); digitalWrite(PIN_ADC_CTRL, HIGH); diff --git a/variants/heltec_e290/HeltecE290Board.h b/variants/heltec_e290/HeltecE290Board.h index f287227c6e..645ec3481a 100644 --- a/variants/heltec_e290/HeltecE290Board.h +++ b/variants/heltec_e290/HeltecE290Board.h @@ -3,6 +3,7 @@ #include #include #include +#include class HeltecE290Board : public ESP32Board { @@ -12,6 +13,8 @@ class HeltecE290Board : public ESP32Board { HeltecE290Board() : periph_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } void begin(); + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); + void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/heltec_t096/LoRaFEMControl.h b/variants/heltec_t096/LoRaFEMControl.h index a3b5c4ed9b..0ce60fffd8 100644 --- a/variants/heltec_t096/LoRaFEMControl.h +++ b/variants/heltec_t096/LoRaFEMControl.h @@ -17,6 +17,6 @@ class LoRaFEMControl bool isLNAEnabled(void) const { return lna_enabled; } private: - bool lna_enabled = true; + bool lna_enabled = false; bool lna_can_control = false; }; diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index 5f17436ad6..e820bf58d3 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/heltec_t096 -I src/helpers/ui -D HELTEC_T096 -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=21 -D P_LORA_NSS=5 -D P_LORA_RESET=16 @@ -120,7 +120,7 @@ build_src_filter = ${Heltec_t096.build_src_filter} lib_deps = ${Heltec_t096.lib_deps} -[env:Heltec_t096_companion_radio_ble_femon] +[env:Heltec_t096_companion_radio_ble] extends = Heltec_t096 board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 712704 @@ -143,12 +143,6 @@ lib_deps = ${Heltec_t096.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Heltec_t096_companion_radio_ble_femoff] -extends = env:Heltec_t096_companion_radio_ble_femon -build_flags = - ${env:Heltec_t096_companion_radio_ble_femon.build_flags} - -D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off - [env:Heltec_t096_companion_radio_usb] extends = Heltec_t096 board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index e808d6c287..135babb1a2 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -12,7 +12,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/heltec_t114 -I src/helpers/ui -D HELTEC_T114 -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=20 -D P_LORA_NSS=24 -D P_LORA_RESET=25 diff --git a/variants/heltec_t190/HeltecT190Board.cpp b/variants/heltec_t190/HeltecT190Board.cpp index 0a16b52b8d..4f35be400b 100644 --- a/variants/heltec_t190/HeltecT190Board.cpp +++ b/variants/heltec_t190/HeltecT190Board.cpp @@ -20,6 +20,33 @@ void HeltecT190Board::begin() { } } + void HeltecT190Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + + void HeltecT190Board::powerOff() { + enterDeepSleep(0); + } + uint16_t HeltecT190Board::getBattMilliVolts() { analogReadResolution(10); digitalWrite(PIN_ADC_CTRL, HIGH); diff --git a/variants/heltec_t190/HeltecT190Board.h b/variants/heltec_t190/HeltecT190Board.h index 557c070ecd..bc38c1e090 100644 --- a/variants/heltec_t190/HeltecT190Board.h +++ b/variants/heltec_t190/HeltecT190Board.h @@ -3,6 +3,7 @@ #include #include #include +#include class HeltecT190Board : public ESP32Board { @@ -12,6 +13,8 @@ class HeltecT190Board : public ESP32Board { HeltecT190Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { } void begin(); + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); + void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/heltec_tracker/platformio.ini b/variants/heltec_tracker/platformio.ini index 2cd0cea6a8..69293d7070 100644 --- a/variants/heltec_tracker/platformio.ini +++ b/variants/heltec_tracker/platformio.ini @@ -99,10 +99,6 @@ lib_deps = ${Heltec_tracker_base.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Heltec_Wireless_Tracker_companion_radio_ble_ps] -extends = env:Heltec_Wireless_Tracker_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Heltec_Wireless_Tracker_repeater] extends = Heltec_tracker_base build_flags = diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp index 99b1cdfe08..f182c905e6 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp @@ -35,12 +35,33 @@ void HeltecTrackerV2Board::begin() { loRaFEMControl.setRxModeEnable(); } - void HeltecTrackerV2Board::powerOff() { - // Turn off PA - digitalWrite(P_LORA_PA_POWER, LOW); - rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_POWER); + void HeltecTrackerV2Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - ESP32Board::powerOff(); + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + loRaFEMControl.setRxModeEnableWhenMCUSleep();//It also needs to be enabled in receive mode + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + + void HeltecTrackerV2Board::powerOff() { + enterDeepSleep(0); } uint16_t HeltecTrackerV2Board::getBattMilliVolts() { diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h index 2bd6a02544..ccbecc7ab6 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h @@ -3,6 +3,7 @@ #include #include #include +#include #include "LoRaFEMControl.h" class HeltecTrackerV2Board : public ESP32Board { @@ -16,6 +17,7 @@ class HeltecTrackerV2Board : public ESP32Board { void begin(); void onBeforeTransmit(void) override; void onAfterTransmit(void) override; + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/heltec_tracker_v2/LoRaFEMControl.h b/variants/heltec_tracker_v2/LoRaFEMControl.h index a3b5c4ed9b..0ce60fffd8 100644 --- a/variants/heltec_tracker_v2/LoRaFEMControl.h +++ b/variants/heltec_tracker_v2/LoRaFEMControl.h @@ -17,6 +17,6 @@ class LoRaFEMControl bool isLNAEnabled(void) const { return lna_enabled; } private: - bool lna_enabled = true; + bool lna_enabled = false; bool lna_can_control = false; }; diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index d914ce6a1b..d57c2113f0 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -176,10 +176,6 @@ lib_deps = ${Heltec_tracker_v2.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:heltec_tracker_v2_companion_radio_ble_ps] -extends = env:heltec_tracker_v2_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:heltec_tracker_v2_companion_radio_wifi] extends = Heltec_tracker_v2 build_flags = diff --git a/variants/heltec_v2/HeltecV2Board.h b/variants/heltec_v2/HeltecV2Board.h index 9b08fe9430..fe800890b8 100644 --- a/variants/heltec_v2/HeltecV2Board.h +++ b/variants/heltec_v2/HeltecV2Board.h @@ -7,6 +7,8 @@ #define PIN_VBAT_READ 37 #define PIN_LED_BUILTIN 25 +#include + class HeltecV2Board : public ESP32Board { public: void begin() { @@ -24,6 +26,29 @@ class HeltecV2Board : public ESP32Board { } } + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_0, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_0); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_0), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_0) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + uint16_t getBattMilliVolts() override { analogReadResolution(10); diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index e9cf56f053..ba4f869422 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -172,10 +172,6 @@ lib_deps = ${Heltec_lora32_v2.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Heltec_v2_companion_radio_ble_ps] -extends = env:Heltec_v2_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Heltec_v2_companion_radio_wifi] extends = Heltec_lora32_v2 build_flags = diff --git a/variants/heltec_v3/HeltecV3Board.h b/variants/heltec_v3/HeltecV3Board.h index 7e7abe313b..ba22a7f2b9 100644 --- a/variants/heltec_v3/HeltecV3Board.h +++ b/variants/heltec_v3/HeltecV3Board.h @@ -17,6 +17,8 @@ #define PIN_ADC_CTRL_ACTIVE LOW #define PIN_ADC_CTRL_INACTIVE HIGH +#include + class HeltecV3Board : public ESP32Board { private: bool adc_active_state; @@ -50,6 +52,33 @@ class HeltecV3Board : public ESP32Board { } } + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + + void powerOff() override { + enterDeepSleep(0); + } + uint16_t getBattMilliVolts() override { analogReadResolution(10); digitalWrite(PIN_ADC_CTRL, adc_active_state); diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 69e4ed050b..a70a93a508 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -179,10 +179,6 @@ lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Heltec_v3_companion_radio_ble_ps] -extends = env:Heltec_v3_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Heltec_v3_companion_radio_wifi] extends = Heltec_lora32_v3 build_flags = @@ -324,10 +320,6 @@ lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Heltec_WSL3_companion_radio_ble_ps] -extends = env:Heltec_WSL3_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Heltec_WSL3_companion_radio_usb] extends = Heltec_lora32_v3 build_flags = diff --git a/variants/heltec_v4/HeltecV4Board.h b/variants/heltec_v4/HeltecV4Board.h index 55166bb37f..fc37b9f6ae 100644 --- a/variants/heltec_v4/HeltecV4Board.h +++ b/variants/heltec_v4/HeltecV4Board.h @@ -3,6 +3,7 @@ #include #include #include +#include #include "LoRaFEMControl.h" #ifndef ADC_MULTIPLIER @@ -22,6 +23,7 @@ class HeltecV4Board : public ESP32Board { void begin(); void onBeforeTransmit(void) override; void onAfterTransmit(void) override; + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); void powerOff() override; bool setLoRaFemLnaEnabled(bool enable) override; bool canControlLoRaFemLna() const override; diff --git a/variants/heltec_v4/LoRaFEMControl.h b/variants/heltec_v4/LoRaFEMControl.h index d84ebe9c6a..d312762d3d 100644 --- a/variants/heltec_v4/LoRaFEMControl.h +++ b/variants/heltec_v4/LoRaFEMControl.h @@ -24,7 +24,7 @@ class LoRaFEMControl LoRaFEMType getFEMType(void) const { return fem_type; } private: LoRaFEMType fem_type=OTHER_FEM_TYPES; - bool lna_enabled=true; + bool lna_enabled=false; bool lna_can_control=false; }; diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index caa71a33f7..fabf38272d 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -222,16 +222,6 @@ lib_deps = ${heltec_v4_oled.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:heltec_v4_companion_radio_ble_ps_femon] -extends = env:heltec_v4_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - -[env:heltec_v4_3_companion_radio_ble_ps_femoff] -extends = env:heltec_v4_companion_radio_ble_ps_femon -build_flags = - ${env:heltec_v4_companion_radio_ble_ps_femon.build_flags} - -D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off - [env:heltec_v4_companion_radio_wifi] extends = heltec_v4_oled build_flags = @@ -396,14 +386,6 @@ lib_deps = ${heltec_v4_tft.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:heltec_v4_expansionkit_tft_companion_radio_ble_ps] -extends = env:heltec_v4_tft_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 -build_flags = - ${env:heltec_v4_tft_companion_radio_ble.build_flags} - -D ENV_PIN_SDA=4 - -D ENV_PIN_SCL=3 - [env:heltec_v4_tft_companion_radio_wifi] extends = heltec_v4_tft build_flags = diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index e7a107a5e9..48723d169a 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -64,10 +64,6 @@ lib_deps = densaugeo/base64 @ ~1.4.0 bakercp/CRC32 @ ^2.0.0 -[env:Heltec_Wireless_Paper_companion_radio_ble_ps] -extends = env:Heltec_Wireless_Paper_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Heltec_Wireless_Paper_companion_radio_usb] extends = Heltec_Wireless_Paper_base build_flags = diff --git a/variants/lilygo_t3s3/platformio.ini b/variants/lilygo_t3s3/platformio.ini index e7f9f11f95..54990117cc 100644 --- a/variants/lilygo_t3s3/platformio.ini +++ b/variants/lilygo_t3s3/platformio.ini @@ -174,10 +174,6 @@ lib_deps = ${LilyGo_T3S3_sx1262.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:LilyGo_T3S3_sx1262_companion_radio_ble_ps] -extends = env:LilyGo_T3S3_sx1262_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:LilyGo_T3S3_sx1262_kiss_modem] extends = LilyGo_T3S3_sx1262 build_src_filter = ${LilyGo_T3S3_sx1262.build_src_filter} diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index 4516e4cd7b..c7a595520e 100644 --- a/variants/lilygo_tbeam_1w/platformio.ini +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -146,11 +146,6 @@ lib_deps = ${LilyGo_TBeam_1W.lib_deps} densaugeo/base64 @ ~1.4.0 -; === LILYGO T-Beam 1W Companion Radio PS (BLE PS) === -[env:LilyGo_TBeam_1W_companion_radio_ble_ps] -extends = env:LilyGo_TBeam_1W_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - ; === LILYGO T-Beam 1W Companion Radio (WiFi) === [env:LilyGo_TBeam_1W_companion_radio_wifi] extends = LilyGo_TBeam_1W diff --git a/variants/lilygo_tbeam_SX1262/platformio.ini b/variants/lilygo_tbeam_SX1262/platformio.ini index 5bc0efddb3..62ac09f875 100644 --- a/variants/lilygo_tbeam_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_SX1262/platformio.ini @@ -65,13 +65,6 @@ lib_deps = ${LilyGo_TBeam_SX1262.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Tbeam_SX1262_companion_radio_ble_ps] -extends = env:Tbeam_SX1262_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 -build_flags = - ${env:Tbeam_SX1262_companion_radio_ble.build_flags} - -D MAX_CONTACTS=150 - [env:Tbeam_SX1262_repeater] extends = LilyGo_TBeam_SX1262 build_flags = diff --git a/variants/lilygo_tbeam_SX1276/platformio.ini b/variants/lilygo_tbeam_SX1276/platformio.ini index edf84e21bf..cb25903ce9 100644 --- a/variants/lilygo_tbeam_SX1276/platformio.ini +++ b/variants/lilygo_tbeam_SX1276/platformio.ini @@ -61,10 +61,6 @@ lib_deps = ${LilyGo_TBeam_SX1276.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Tbeam_SX1276_companion_radio_ble_ps] -extends = env:Tbeam_SX1276_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Tbeam_SX1276_repeater] extends = LilyGo_TBeam_SX1276 build_flags = diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index b930a25b93..0294615651 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -142,10 +142,6 @@ lib_deps = ${T_Beam_S3_Supreme_SX1262.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:T_Beam_S3_Supreme_SX1262_companion_radio_ble_ps] -extends = env:T_Beam_S3_Supreme_SX1262_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:T_Beam_S3_Supreme_SX1262_companion_radio_wifi] extends = T_Beam_S3_Supreme_SX1262 build_flags = diff --git a/variants/lilygo_tdeck/TDeckBoard.h b/variants/lilygo_tdeck/TDeckBoard.h index e2844360df..7ed007af9c 100644 --- a/variants/lilygo_tdeck/TDeckBoard.h +++ b/variants/lilygo_tdeck/TDeckBoard.h @@ -3,6 +3,7 @@ #include #include #include "helpers/ESP32Board.h" +#include #define PIN_VBAT_READ 4 #define BATTERY_SAMPLES 8 @@ -22,6 +23,29 @@ class TDeckBoard : public ESP32Board { } #endif + void enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + uint16_t getBattMilliVolts() { #if defined(PIN_VBAT_READ) && defined(ADC_MULTIPLIER) analogReadResolution(12); diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index bb173524c4..3673166861 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -92,7 +92,7 @@ extends = LilyGo_TLora_V2_1_1_6 build_flags = ${LilyGo_TLora_V2_1_1_6.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=100 + -D MAX_CONTACTS=160 -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 -D OFFLINE_QUEUE_SIZE=128 @@ -108,10 +108,6 @@ lib_deps = ${LilyGo_TLora_V2_1_1_6.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:LilyGo_TLora_V2_1_1_6_companion_radio_ble_ps] -extends = env:LilyGo_TLora_V2_1_1_6_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:LilyGo_TLora_V2_1_1_6_room_server] extends = LilyGo_TLora_V2_1_1_6 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter} diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index 1e1a32740f..5415e15861 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -1,7 +1,6 @@ [Promicro] extends = nrf52_base board = promicro_nrf52840 -board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} -I variants/promicro -D PROMICRO diff --git a/variants/rak3112/RAK3112Board.h b/variants/rak3112/RAK3112Board.h index 704162b8c8..8ba3197cf6 100644 --- a/variants/rak3112/RAK3112Board.h +++ b/variants/rak3112/RAK3112Board.h @@ -16,6 +16,8 @@ #define ADC_MULTIPLIER (3 * 1.73 * 1.187 * 1000) #define BATTERY_SAMPLES 8 +#include + class RAK3112Board : public ESP32Board { private: bool adc_active_state; @@ -49,6 +51,33 @@ class RAK3112Board : public ESP32Board { } } + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + + void powerOff() override { + enterDeepSleep(0); + } + uint16_t getBattMilliVolts() override { analogReadResolution(12); diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 5eeb6a9d6c..20a8a548b9 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -2,12 +2,11 @@ extends = nrf52_base board = rak3401 board_check = true -board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak3401 -D RAK_3401 -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 8f72999af4..2bbba31463 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -2,7 +2,6 @@ extends = nrf52_base board = rak4631 board_check = true -board_build.ldscript = boards/nrf52840_s140_v6.ld extra_scripts = ${nrf52_base.extra_scripts} post:variants/rak4631/fix_bsec_lib.py build_flags = ${nrf52_base.build_flags} @@ -10,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/rak4631 -D RAK_4631 -D RAK_BOARD -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_GPS_TX=PIN_SERIAL1_RX diff --git a/variants/rak_wismesh_tag/platformio.ini b/variants/rak_wismesh_tag/platformio.ini index b596c9a659..e9cddb74dd 100644 --- a/variants/rak_wismesh_tag/platformio.ini +++ b/variants/rak_wismesh_tag/platformio.ini @@ -2,7 +2,6 @@ extends = nrf52_base board = rak4631 board_check = true -board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak_wismesh_tag diff --git a/variants/sensecap_solar/platformio.ini b/variants/sensecap_solar/platformio.ini index c9f7ed397d..effef38ccd 100644 --- a/variants/sensecap_solar/platformio.ini +++ b/variants/sensecap_solar/platformio.ini @@ -10,7 +10,7 @@ build_flags = ${nrf52_base.build_flags} -I src/helpers/nrf52 -D NRF52_PLATFORM=1 -D USE_SX1262 -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D P_LORA_TX_LED=12 diff --git a/variants/station_g2/StationG2Board.h b/variants/station_g2/StationG2Board.h index d1989ee0ff..a905682c8d 100644 --- a/variants/station_g2/StationG2Board.h +++ b/variants/station_g2/StationG2Board.h @@ -2,6 +2,7 @@ #include #include +#include class StationG2Board : public ESP32Board { public: @@ -20,6 +21,29 @@ class StationG2Board : public ESP32Board { } } + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + uint16_t getBattMilliVolts() override { return 0; } diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index 87e77152b8..6432b52386 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -238,3 +238,8 @@ build_src_filter = ${Station_G2.build_src_filter} lib_deps = ${Station_G2.lib_deps} densaugeo/base64 @ ~1.4.0 + +[env:Station_G2_kiss_modem] +extends = Station_G2 +build_src_filter = ${Station_G2.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/station_g2/target.cpp b/variants/station_g2/target.cpp index 40fa80056b..8018c20eb8 100644 --- a/variants/station_g2/target.cpp +++ b/variants/station_g2/target.cpp @@ -40,21 +40,6 @@ bool radio_init() { #endif } -uint32_t radio_get_rng_seed() { - return radio.random(0x7FFFFFFF); -} - -void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { - radio.setFrequency(freq); - radio.setSpreadingFactor(sf); - radio.setBandwidth(bw); - radio.setCodingRate(cr); -} - -void radio_set_tx_power(int8_t dbm) { - radio.setOutputPower(dbm); -} - mesh::LocalIdentity radio_new_identity() { RadioNoiseListener rng(radio); return mesh::LocalIdentity(&rng); // create new random identity diff --git a/variants/station_g2/target.h b/variants/station_g2/target.h index 9a3610252c..b4eadbba31 100644 --- a/variants/station_g2/target.h +++ b/variants/station_g2/target.h @@ -24,8 +24,5 @@ extern EnvironmentSensorManager sensors; #endif bool radio_init(); -uint32_t radio_get_rng_seed(); -void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); -void radio_set_tx_power(int8_t dbm); mesh::LocalIdentity radio_new_identity(); diff --git a/variants/thinknode_m2/ThinknodeM2Board.cpp b/variants/thinknode_m2/ThinknodeM2Board.cpp index 8d68006d2c..0596510361 100644 --- a/variants/thinknode_m2/ThinknodeM2Board.cpp +++ b/variants/thinknode_m2/ThinknodeM2Board.cpp @@ -1,30 +1,40 @@ #include "ThinknodeM2Board.h" + + void ThinknodeM2Board::begin() { - pinMode(PIN_VEXT_EN, OUTPUT); - digitalWrite(PIN_VEXT_EN, !PIN_VEXT_EN_ACTIVE); // force power cycle - delay(20); // allow power rail to discharge - digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE); // turn backlight back on - delay(120); // give display time to bias on cold boot - ESP32Board::begin(); - pinMode(PIN_STATUS_LED, OUTPUT); // init power led -} + pinMode(PIN_VEXT_EN, OUTPUT); + digitalWrite(PIN_VEXT_EN, !PIN_VEXT_EN_ACTIVE); // force power cycle + delay(20); // allow power rail to discharge + digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE); // turn backlight back on + delay(120); // give display time to bias on cold boot + ESP32Board::begin(); + pinMode(PIN_STATUS_LED, OUTPUT); // init power led + } -uint16_t ThinknodeM2Board::getBattMilliVolts() { - analogReadResolution(12); - analogSetPinAttenuation(PIN_VBAT_READ, ADC_11db); + void ThinknodeM2Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_deep_sleep_start(); + } - uint32_t mv = 0; - for (int i = 0; i < 8; ++i) { - mv += analogReadMilliVolts(PIN_VBAT_READ); - delayMicroseconds(200); + void ThinknodeM2Board::powerOff() { + enterDeepSleep(0); } - mv /= 8; - analogReadResolution(10); - return static_cast(mv * ADC_MULTIPLIER); -} + uint16_t ThinknodeM2Board::getBattMilliVolts() { + analogReadResolution(12); + analogSetPinAttenuation(PIN_VBAT_READ, ADC_11db); + + uint32_t mv = 0; + for (int i = 0; i < 8; ++i) { + mv += analogReadMilliVolts(PIN_VBAT_READ); + delayMicroseconds(200); + } + mv /= 8; -const char *ThinknodeM2Board::getManufacturerName() const { - return "Elecrow ThinkNode M2"; + analogReadResolution(10); + return static_cast(mv * ADC_MULTIPLIER ); } + + const char* ThinknodeM2Board::getManufacturerName() const { + return "Elecrow ThinkNode M2"; + } diff --git a/variants/thinknode_m2/ThinknodeM2Board.h b/variants/thinknode_m2/ThinknodeM2Board.h index 0255677784..8011fae67d 100644 --- a/variants/thinknode_m2/ThinknodeM2Board.h +++ b/variants/thinknode_m2/ThinknodeM2Board.h @@ -3,11 +3,15 @@ #include #include #include +#include class ThinknodeM2Board : public ESP32Board { public: + void begin(); + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); + void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/thinknode_m5/ThinknodeM5Board.cpp b/variants/thinknode_m5/ThinknodeM5Board.cpp index 2cb138e639..c4de538c56 100644 --- a/variants/thinknode_m5/ThinknodeM5Board.cpp +++ b/variants/thinknode_m5/ThinknodeM5Board.cpp @@ -19,6 +19,14 @@ void ThinknodeM5Board::begin() { ESP32Board::begin(); } + void ThinknodeM5Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_deep_sleep_start(); + } + + void ThinknodeM5Board::powerOff() { + enterDeepSleep(0); + } + uint16_t ThinknodeM5Board::getBattMilliVolts() { analogReadResolution(12); analogSetPinAttenuation(PIN_VBAT_READ, ADC_11db); diff --git a/variants/thinknode_m5/ThinknodeM5Board.h b/variants/thinknode_m5/ThinknodeM5Board.h index 57ff0d00eb..3c120027b7 100644 --- a/variants/thinknode_m5/ThinknodeM5Board.h +++ b/variants/thinknode_m5/ThinknodeM5Board.h @@ -3,6 +3,7 @@ #include #include #include +#include #include extern PCA9557 expander; @@ -12,6 +13,8 @@ class ThinknodeM5Board : public ESP32Board { public: void begin(); + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); + void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/xiao_c3/XiaoC3Board.h b/variants/xiao_c3/XiaoC3Board.h index c5700c6884..6ea1c15ffc 100644 --- a/variants/xiao_c3/XiaoC3Board.h +++ b/variants/xiao_c3/XiaoC3Board.h @@ -3,6 +3,7 @@ #include #include +#include #include class XiaoC3Board : public ESP32Board { @@ -39,6 +40,37 @@ class XiaoC3Board : public ESP32Board { #endif } + void enterDeepSleep(uint32_t secs, int8_t wake_pin = -1) { + gpio_set_direction(gpio_num_t(P_LORA_DIO_1), GPIO_MODE_INPUT); + if (wake_pin >= 0) { + gpio_set_direction((gpio_num_t)wake_pin, GPIO_MODE_INPUT); + } + + //hold disable, isolate and power domain config functions may be unnecessary + //gpio_deep_sleep_hold_dis(); + //esp_sleep_config_gpio_isolate(); + gpio_deep_sleep_hold_en(); + +#if defined(LORA_TX_BOOST_PIN) + gpio_hold_en((gpio_num_t) LORA_TX_BOOST_PIN); + gpio_deep_sleep_hold_en(); +#endif + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + if (wake_pin >= 0) { + esp_deep_sleep_enable_gpio_wakeup((1 << P_LORA_DIO_1) | (1 << wake_pin), ESP_GPIO_WAKEUP_GPIO_HIGH); + } else { + esp_deep_sleep_enable_gpio_wakeup(1 << P_LORA_DIO_1, ESP_GPIO_WAKEUP_GPIO_HIGH); + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + #if defined(LORA_TX_BOOST_PIN) || defined(P_LORA_TX_LED) void onBeforeTransmit() override { #if defined(P_LORA_TX_LED) diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index fa70d8a091..c0e8458d0e 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -90,11 +90,6 @@ lib_deps = ${esp32_ota.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Xiao_C3_companion_radio_ble_ps] -extends = env:Xiao_C3_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 -board_build.partitions = min_spiffs.csv ; get around 4mb flash limit - [env:Xiao_C3_companion_radio_usb] extends = Xiao_esp32_C3 build_src_filter = ${Xiao_esp32_C3.build_src_filter} diff --git a/variants/xiao_c6/platformio.ini b/variants/xiao_c6/platformio.ini index 0d8c2a79d3..9f504b8ed0 100644 --- a/variants/xiao_c6/platformio.ini +++ b/variants/xiao_c6/platformio.ini @@ -1,7 +1,6 @@ [Xiao_C6] extends = esp32c6_base board = esp32-c6-devkitm-1 -board_build.flash_mode = dio board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_flags = ${esp32c6_base.build_flags} diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index f4d1b93e6b..a085433688 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/xiao_nrf52 -UENV_INCLUDE_GPS -D NRF52_PLATFORM -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D XIAO_NRF52 -D USE_SX1262 -D RADIO_CLASS=CustomSX1262 diff --git a/variants/xiao_s3/platformio.ini b/variants/xiao_s3/platformio.ini index b59e69ae43..22464e7d80 100644 --- a/variants/xiao_s3/platformio.ini +++ b/variants/xiao_s3/platformio.ini @@ -114,10 +114,6 @@ lib_deps = densaugeo/base64 @ ~1.4.0 adafruit/Adafruit SSD1306 @ ^2.5.13 -[env:Xiao_S3_companion_radio_ble_ps] -extends = env:Xiao_S3_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Xiao_S3_companion_radio_usb] extends = Xiao_S3 build_flags = diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index 41420acf5d..db8c5a9486 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -173,10 +173,6 @@ lib_deps = densaugeo/base64 @ ~1.4.0 adafruit/Adafruit SSD1306 @ ^2.5.13 -[env:Xiao_S3_WIO_companion_radio_ble_ps] -extends = env:Xiao_S3_WIO_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Xiao_S3_WIO_companion_radio_serial] extends = Xiao_S3_WIO build_flags = From 2acd61ec84988bbbba006d8fc7ae747f852c368f Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 16:02:22 -0700 Subject: [PATCH 82/94] Remove PowerSaving changes from Halo retry branch --- .github/workflows/stale-bot.yml | 32 ----- SECURITY.md | 57 -------- build-iotthinks.sh | 135 ------------------ examples/companion_radio/DataStore.cpp | 2 - examples/companion_radio/MyMesh.cpp | 29 ---- examples/companion_radio/MyMesh.h | 2 +- examples/companion_radio/NodePrefs.h | 3 +- examples/companion_radio/main.cpp | 40 ------ examples/simple_room_server/MyMesh.cpp | 8 -- examples/simple_room_server/MyMesh.h | 3 - examples/simple_room_server/main.cpp | 13 -- src/MeshCore.h | 4 + src/helpers/ArduinoHelpers.cpp | 6 - src/helpers/ArduinoHelpers.h | 42 +----- src/helpers/ArduinoSerialInterface.cpp | 4 - src/helpers/ArduinoSerialInterface.h | 3 +- src/helpers/BaseChatMesh.h | 10 +- src/helpers/BaseSerialInterface.h | 1 - src/helpers/ESP32Board.h | 106 ++------------ src/helpers/MeshadventurerBoard.h | 30 ++++ src/helpers/NRF52Board.cpp | 28 ++-- src/helpers/NRF52Board.h | 4 +- src/helpers/SensorManager.h | 2 - src/helpers/esp32/SerialBLEInterface.cpp | 4 - src/helpers/esp32/SerialBLEInterface.h | 1 - src/helpers/esp32/SerialWifiInterface.cpp | 4 - src/helpers/esp32/SerialWifiInterface.h | 1 - src/helpers/esp32/TBeamBoard.h | 24 ++++ src/helpers/nrf52/SerialBLEInterface.cpp | 4 - src/helpers/nrf52/SerialBLEInterface.h | 1 - .../sensors/EnvironmentSensorManager.cpp | 16 --- .../sensors/EnvironmentSensorManager.h | 1 - src/helpers/ui/ST7735Display.cpp | 9 -- 33 files changed, 93 insertions(+), 536 deletions(-) delete mode 100644 .github/workflows/stale-bot.yml delete mode 100644 SECURITY.md delete mode 100644 build-iotthinks.sh delete mode 100644 src/helpers/ArduinoHelpers.cpp diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml deleted file mode 100644 index ec16658756..0000000000 --- a/.github/workflows/stale-bot.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: 'Run Stale Bot' -on: - schedule: - - cron: '30 1 * * *' # daily at 1:30am - workflow_dispatch: {} - -permissions: - actions: write - issues: write - pull-requests: write - -jobs: - close-issues: - # only run on main repo, not forks - if: github.repository == 'meshcore-dev/MeshCore' - runs-on: ubuntu-latest - steps: - - name: Close Stale Issues - uses: actions/stale@v10 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - # auto close issues - days-before-issue-stale: 60 - days-before-issue-close: 7 - exempt-issue-labels: "keep-open" - stale-issue-label: "stale" - stale-issue-message: "This issue is stale because it has been open for 60 days with no activity. Remove the stale label or add a comment if this issue is still relevant, otherwise this issue will automatically close in 7 days." - close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale." - # don't auto close prs - days-before-pr-stale: -1 - days-before-pr-close: -1 - \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index a4b2207d55..0000000000 --- a/SECURITY.md +++ /dev/null @@ -1,57 +0,0 @@ -# Security Policy - -## Supported Versions - -Security fixes are applied to the latest release only. We do not backport -fixes to older versions. - -| Version | Supported | -|---------|-----------| -| 1.15+ | ✅ | -| <1.15 | ❌ | - -## Reporting a Vulnerability - -**Please do not report security vulnerabilities through public GitHub issues.** - -Use GitHub's private vulnerability reporting instead: -1. Go to the **Security** tab of this repository -2. Click **Report a vulnerability** -3. Fill in the details and submit - -### What to include - -A useful report tells us: -- Which component or file is affected -- What an attacker can do (impact) and under what conditions -- A minimal reproduction case or proof-of-concept if you have one -- Whether you believe it is remotely exploitable - -You do not need a working exploit to report. An incomplete report is better -than no report. - -## What to expect - -This is a volunteer-maintained open-source project. We will do our best to -respond in a reasonable timeframe, but cannot commit to specific deadlines. - -We ask that you give us a fair opportunity to investigate and address the -issue before any public disclosure. If you have not heard back after -**90 days**, feel free to follow up or proceed with disclosure at your -discretion. - -## Scope - -In scope: -- Remote code execution, memory corruption, or denial-of-service via crafted - radio packets -- Authentication or encryption bypasses -- Vulnerabilities in the packet routing or path handling logic - -Out of scope: -- Physical access attacks (e.g., JTAG, UART extraction of keys) -- Regulatory compliance (duty cycle, frequency restrictions) -- Jamming or other physical-layer radio interference -- Issues in third-party libraries (RadioLib, Crypto, etc.) — report those - upstream -- "Best practice" suggestions without a demonstrated attack path diff --git a/build-iotthinks.sh b/build-iotthinks.sh deleted file mode 100644 index 613789d08a..0000000000 --- a/build-iotthinks.sh +++ /dev/null @@ -1,135 +0,0 @@ -# sh ./build-repeaters-iotthinks.sh -export FIRMWARE_VERSION="PowerSaving16" - -############# Repeaters ############# -# Commonly-used boards -## ESP32 - 17 boards -sh build.sh build-firmware \ -Heltec_v3_repeater \ -Heltec_WSL3_repeater \ -heltec_v4_repeater \ -Station_G2_repeater \ -T_Beam_S3_Supreme_SX1262_repeater \ -Tbeam_SX1262_repeater \ -LilyGo_T3S3_sx1262_repeater \ -Xiao_S3_WIO_repeater \ -Xiao_C3_repeater \ -Xiao_C6_repeater_ \ -Heltec_E290_repeater \ -Heltec_Wireless_Tracker_repeater \ -LilyGo_TBeam_1W_repeater \ -Xiao_S3_repeater \ -heltec_tracker_v2_repeater \ -Heltec_Wireless_Paper_repeater \ -Heltec_ct62_repeater - -## NRF52 - 17 boards -sh build.sh build-firmware \ -RAK_4631_repeater \ -Heltec_t114_repeater \ -Xiao_nrf52_repeater \ -Heltec_mesh_solar_repeater \ -ProMicro_repeater \ -SenseCap_Solar_repeater \ -t1000e_repeater \ -LilyGo_T-Echo_repeater \ -WioTrackerL1_repeater \ -RAK_3401_repeater \ -RAK_WisMesh_Tag_repeater \ -GAT562_30S_Mesh_Kit_repeater \ -GAT562_Mesh_Tracker_Pro_repeater \ -ikoka_nano_nrf_22dbm_repeater \ -ikoka_nano_nrf_30dbm_repeater \ -ikoka_nano_nrf_33dbm_repeater \ -ThinkNode_M1_repeater \ -Heltec_t096_repeater - -## ESP32, SX1276 - 3 boards -sh build.sh build-firmware \ -Heltec_v2_repeater \ -LilyGo_TLora_V2_1_1_6_repeater \ -Tbeam_SX1276_repeater - -############# Room Server ############# -# ESP32 - 7 boards -sh build.sh build-firmware \ -Heltec_v3_room_server \ -heltec_v4_room_server \ -LilyGo_TBeam_1W_room_server \ -Heltec_WSL3_room_server \ -Xiao_S3_room_server \ -heltec_tracker_v2_room_server \ -Heltec_Wireless_Paper_room_server - -# NRF52 - 6 boards -sh build.sh build-firmware \ -RAK_4631_room_server \ -Heltec_t114_room_server \ -Xiao_nrf52_room_server \ -t1000e_room_server \ -WioTrackerL1_room_server \ -RAK_3401_room_server \ -Heltec_t096_room_server - -############# Companions BLE ############# -# NRF52 - 12 boards -sh build.sh build-firmware \ -RAK_4631_companion_radio_ble \ -Heltec_t114_companion_radio_ble \ -Xiao_nrf52_companion_radio_ble \ -t1000e_companion_radio_ble \ -LilyGo_T-Echo_companion_radio_ble \ -WioTrackerL1_companion_radio_ble \ -RAK_3401_companion_radio_ble \ -RAK_WisMesh_Tag_companion_radio_ble \ -SenseCap_Solar_companion_radio_ble \ -ThinkNode_M1_companion_radio_ble \ -Heltec_t096_companion_radio_ble \ -Heltec_t096_companion_radio_ble_femoff - -############# Companions BLE PS ############# -# ESP32 - 18 boards -sh build.sh build-firmware \ -Heltec_v3_companion_radio_ble_ps \ -heltec_v4_companion_radio_ble_ps \ -heltec_v4_3_companion_radio_ble_ps_femoff \ -Xiao_C3_companion_radio_ble_ps \ -Xiao_S3_companion_radio_ble_ps \ -Xiao_S3_WIO_companion_radio_ble_ps \ -Heltec_v2_companion_radio_ble_ps \ -LilyGo_TBeam_1W_companion_radio_ble_ps \ -Heltec_WSL3_companion_radio_ble_ps \ -Heltec_Wireless_Tracker_companion_radio_ble_ps \ -heltec_tracker_v2_companion_radio_ble_ps \ -Heltec_Wireless_Paper_companion_radio_ble_ps \ -LilyGo_TLora_V2_1_1_6_companion_radio_ble_ps \ -Heltec_ct62_companion_radio_ble_ps \ -T_Beam_S3_Supreme_SX1262_companion_radio_ble_ps \ -Tbeam_SX1262_companion_radio_ble_ps \ -heltec_v4_expansionkit_tft_companion_radio_ble_ps \ -LilyGo_T3S3_sx1262_companion_radio_ble_ps - -# Not working -Tbeam_SX1276_companion_radio_ble_ps \ - -############# Companions USB ############# -sh build.sh build-firmware \ -Heltec_t096_companion_radio_usb - -############# Sample builds ############# -# 14 boards -sh build.sh build-firmware \ -Heltec_v3_repeater \ -heltec_v4_repeater \ -Xiao_C3_repeater \ -Xiao_C6_repeater_ \ -RAK_4631_repeater \ -Heltec_t096_repeater \ -Heltec_v3_companion_radio_ble_ps \ -heltec_v4_companion_radio_ble_ps \ -heltec_v4_3_companion_radio_ble_ps_femoff \ -Xiao_C3_companion_radio_ble_ps \ -Xiao_C6_companion_radio_ble_ \ -RAK_4631_companion_radio_ble \ -Heltec_t096_companion_radio_ble \ -Heltec_t096_companion_radio_ble_femoff diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index fdb924ad4c..bf2f36c3d9 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -233,7 +233,6 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 - file.read((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122 file.close(); } @@ -274,7 +273,6 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 - file.write((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122 file.close(); } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 5d1adb465a..5fb9bf9d37 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -62,8 +62,6 @@ #define CMD_SET_DEFAULT_FLOOD_SCOPE 63 #define CMD_GET_DEFAULT_FLOOD_SCOPE 64 #define CMD_SEND_RAW_PACKET 65 -#define CMD_GET_RADIO_FEM_RXGAIN 66 -#define CMD_SET_RADIO_FEM_RXGAIN 67 // Stats sub-types for CMD_GET_STATS #define STATS_TYPE_CORE 0 @@ -891,7 +889,6 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.rx_boosted_gain = 1; // enabled by default #endif #endif - _prefs.radio_fem_rxgain = 1; } void MyMesh::begin(bool has_display) { @@ -941,7 +938,6 @@ void MyMesh::begin(bool has_display) { _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER); _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1 _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours - _prefs.radio_fem_rxgain = constrain(_prefs.radio_fem_rxgain, 0, 1); #ifdef BLE_PIN_CODE // 123456 by default if (_prefs.ble_pin == 0) { @@ -971,7 +967,6 @@ void MyMesh::begin(bool has_display) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); - board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); } @@ -1830,30 +1825,6 @@ void MyMesh::handleCmdFrame(size_t len) { } else { writeErrFrame(ERR_CODE_ILLEGAL_ARG); } - } else if (cmd_frame[0] == CMD_GET_RADIO_FEM_RXGAIN) { - if (!board.canControlLoRaFemLna()) { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); - } else { - out_frame[0] = RESP_CODE_OK; - uint8_t value = board.isLoRaFemLnaEnabled() ? 1 : 0; - memcpy(&out_frame[1], &value, 1); - _serial->writeFrame(out_frame, 2); - } - } else if (cmd_frame[0] == CMD_SET_RADIO_FEM_RXGAIN && len >= 2) { - uint8_t value = cmd_frame[1]; - if (!board.canControlLoRaFemLna()) { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); - } else if (value <= 1) { - _prefs.radio_fem_rxgain = value; - if (board.setLoRaFemLnaEnabled(value != 0)) { - savePrefs(); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); - } - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } } else if (cmd_frame[0] == CMD_GET_ADVERT_PATH && len >= PUB_KEY_SIZE+2) { // FUTURE use: uint8_t reserved = cmd_frame[1]; uint8_t *pub_key = &cmd_frame[2]; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 0d687cfd5e..f4190f30ac 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -166,7 +166,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { public: void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); } - + #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0"); diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index ecb117bd2f..48c381ceaf 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -29,10 +29,9 @@ struct NodePrefs { // persisted to file uint32_t gps_interval; // GPS read interval in seconds 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 uint8_t client_repeat; uint8_t path_hash_mode; // which path mode to use when sending uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) char default_scope_name[31]; uint8_t default_scope_key[16]; -}; +}; \ No newline at end of file diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index f10cb17029..ef9b6bfca4 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -2,11 +2,6 @@ #include #include "MyMesh.h" -#ifdef ESP32_PLATFORM -#include "esp_pm.h" -#include "esp_bt.h" -#endif - // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -245,37 +240,6 @@ void setup() { #endif board.onBootComplete(); - -#ifdef ESP32_PLATFORM -#if !CONFIG_IDF_TARGET_ESP32C6 - // Enable BLE sleep - esp_err_t errBLESleep = esp_bt_sleep_enable(); - if (errBLESleep == ESP_OK) { - Serial.println("Bluetooth sleep enabled successfully"); - } else { - Serial.printf("Bluetooth sleep enable failed: %s\n", esp_err_to_name(errBLESleep)); - } -#endif - -#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; -#elif CONFIG_IDF_TARGET_ESP32C6 - esp_pm_config_t pm_config; -#endif - - // Configure Power Management - pm_config = { .max_freq_mhz = 80, .min_freq_mhz = 40, .light_sleep_enable = true }; - esp_err_t errPM = esp_pm_configure(&pm_config); - if (errPM == ESP_OK) { - Serial.println("Power Management configured successfully"); - } else { - Serial.printf("Power Management failed to configure: %d\r\n", errPM); - } -#endif } void loop() { @@ -289,10 +253,6 @@ void loop() { if (!the_mesh.hasPendingWork()) { #if defined(NRF52_PLATFORM) board.sleep(0); // nrf ignores seconds param, sleeps whenever possible -#elif defined(ESP32_PLATFORM) - if (!serial_interface.isReadBusy() && !serial_interface.isWriteBusy()) { // BLE is not busy - vTaskDelay(pdMS_TO_TICKS(10)); // attempt to sleep - } #endif } diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index c311c94194..12d0b0c318 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -1031,11 +1031,3 @@ void MyMesh::loop() { uptime_millis += now - last_millis; last_millis = now; } - -// To check if there is pending work -bool MyMesh::hasPendingWork() const { -#if defined(WITH_BRIDGE) - if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep -#endif - return _mgr->getOutboundTotal() > 0; -} diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 380e54da94..e9e53ec919 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -225,7 +225,4 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void clearStats() override; void handleCommand(uint32_t sender_timestamp, char* command, char* reply); void loop(); - - // To check if there is pending work - bool hasPendingWork() const; }; diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index ad8aa9149d..a3798b2175 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -18,9 +18,6 @@ void halt() { static char command[MAX_POST_TEXT_LEN+1]; -// For power saving -unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot - void setup() { Serial.begin(115200); delay(1000); @@ -118,14 +115,4 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); - - if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { -#if defined(NRF52_PLATFORM) - board.sleep(0); // nrf ignores seconds param, sleeps whenever possible -#else - if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep - board.sleep(30); // Sleep. Wake up after a while or when receiving a LoRa packet - } -#endif - } } diff --git a/src/MeshCore.h b/src/MeshCore.h index 846b8f05b0..89e60b1f7e 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -53,6 +53,10 @@ class MainBoard { virtual void onAfterTransmit() { } virtual void reboot() = 0; virtual void powerOff() { /* no op */ } + // Called by example setup() functions to signal that boot is complete. + // Boards may override to stop a boot-indicator LED sequence or similar. + // 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 uint32_t getGpio() { return 0; } diff --git a/src/helpers/ArduinoHelpers.cpp b/src/helpers/ArduinoHelpers.cpp deleted file mode 100644 index feb77a79c6..0000000000 --- a/src/helpers/ArduinoHelpers.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include - -extern "C" { - __attribute__((section(".persistent_magic"))) uint32_t persistent_magic; - __attribute__((section(".persistent_data"))) uint32_t persistent_time; -} \ No newline at end of file diff --git a/src/helpers/ArduinoHelpers.h b/src/helpers/ArduinoHelpers.h index 9b50b98caf..97596daa31 100644 --- a/src/helpers/ArduinoHelpers.h +++ b/src/helpers/ArduinoHelpers.h @@ -3,57 +3,19 @@ #include #include -#ifdef NRF52_PLATFORM -#define CLOCK_MAGIC_NUM 0xAA55CC33 -#define RTC_TIME_MIN 1772323200 // 1 Mar 2026 - -extern uint32_t persistent_magic; -extern uint32_t persistent_time; -#endif - class VolatileRTCClock : public mesh::RTCClock { uint32_t base_time; uint64_t accumulator; unsigned long prev_millis; - public: - VolatileRTCClock() { -#ifdef NRF52_PLATFORM - if (persistent_magic == CLOCK_MAGIC_NUM && persistent_time >= RTC_TIME_MIN) { - base_time = persistent_time; - } else { - base_time = RTC_TIME_MIN; - } -#else - base_time = 1715770351; -#endif - - accumulator = 0; - prev_millis = millis(); - } - + VolatileRTCClock() { base_time = 1715770351; accumulator = 0; prev_millis = millis(); } // 15 May 2024, 8:50pm uint32_t getCurrentTime() override { return base_time + accumulator/1000; } - - void setCurrentTime(uint32_t time) override { - base_time = time; - accumulator = 0; - prev_millis = millis(); - -#ifdef NRF52_PLATFORM - persistent_magic = CLOCK_MAGIC_NUM; - persistent_time = time; -#endif - } + void setCurrentTime(uint32_t time) override { base_time = time; accumulator = 0; prev_millis = millis(); } void tick() override { unsigned long now = millis(); accumulator += (now - prev_millis); prev_millis = now; - -#ifdef NRF52_PLATFORM - persistent_magic = CLOCK_MAGIC_NUM; - persistent_time = getCurrentTime(); -#endif } }; diff --git a/src/helpers/ArduinoSerialInterface.cpp b/src/helpers/ArduinoSerialInterface.cpp index 6b44397406..a01fa5866f 100644 --- a/src/helpers/ArduinoSerialInterface.cpp +++ b/src/helpers/ArduinoSerialInterface.cpp @@ -17,10 +17,6 @@ bool ArduinoSerialInterface::isConnected() const { return true; // no way of knowing, so assume yes } -bool ArduinoSerialInterface::isReadBusy() const { - return false; -} - bool ArduinoSerialInterface::isWriteBusy() const { return false; } diff --git a/src/helpers/ArduinoSerialInterface.h b/src/helpers/ArduinoSerialInterface.h index 4fa2b75d14..c4086353aa 100644 --- a/src/helpers/ArduinoSerialInterface.h +++ b/src/helpers/ArduinoSerialInterface.h @@ -28,8 +28,7 @@ class ArduinoSerialInterface : public BaseSerialInterface { bool isConnected() const override; - bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; -}; +}; \ No newline at end of file diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index c6027468d0..d987854709 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -38,6 +38,8 @@ class ContactsIterator { #define MAX_CONTACTS 32 #endif +#define MAX_ANON_CONTACTS 8 + #ifndef MAX_CONNECTIONS #define MAX_CONNECTIONS 16 #endif @@ -59,9 +61,9 @@ class BaseChatMesh : public mesh::Mesh { friend class ContactsIterator; - ContactInfo contacts[MAX_CONTACTS]; + ContactInfo contacts[MAX_CONTACTS+MAX_ANON_CONTACTS]; int num_contacts; - int sort_array[MAX_CONTACTS]; + int sort_array[MAX_CONTACTS+MAX_ANON_CONTACTS]; int matching_peer_indexes[MAX_SEARCH_RESULTS]; unsigned long txt_send_timeout; #ifdef MAX_GROUP_CHANNELS @@ -73,7 +75,7 @@ class BaseChatMesh : public mesh::Mesh { ConnectionInfo connections[MAX_CONNECTIONS]; mesh::Packet* composeMsgPacket(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char *text, uint32_t& expected_ack); - void sendAckTo(const ContactInfo& dest, uint32_t ack_hash); + void sendAckTo(const ContactInfo& dest, const uint8_t* ack_hash, uint8_t ack_len=4); protected: BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables) @@ -97,7 +99,7 @@ class BaseChatMesh : public mesh::Mesh { num_contacts = MAX_ANON_CONTACTS; // seed the first contacts for anon requests } void populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp); - ContactInfo* allocateContactSlot(); // helper to find slot for new contact + ContactInfo* allocateContactSlot(bool transient_only=false); // helper to find slot for new contact // 'UI' concepts, for sub-classes to implement virtual bool isAutoAddEnabled() const { return true; } diff --git a/src/helpers/BaseSerialInterface.h b/src/helpers/BaseSerialInterface.h index 8ff110eb57..e9a3f2ab46 100644 --- a/src/helpers/BaseSerialInterface.h +++ b/src/helpers/BaseSerialInterface.h @@ -15,7 +15,6 @@ class BaseSerialInterface { virtual bool isConnected() const = 0; - virtual bool isReadBusy() const = 0; virtual bool isWriteBusy() const = 0; virtual size_t writeFrame(const uint8_t src[], size_t len) = 0; virtual size_t checkRecvFrame(uint8_t dest[]) = 0; diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index 48730fdd46..a4cbf2a980 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -14,7 +14,6 @@ #include #include "soc/rtc.h" #include "esp_system.h" -#include class ESP32Board : public mesh::MainBoard { protected: @@ -25,7 +24,7 @@ class ESP32Board : public mesh::MainBoard { public: void begin() { // for future use, sub-classes SHOULD call this from their begin() - startup_reason = BD_STARTUP_NORMAL; + startup_reason = BD_STARTUP_NORMAL; #ifdef ESP32_CPU_FREQ setCpuFrequencyMhz(ESP32_CPU_FREQ); @@ -48,7 +47,7 @@ class ESP32Board : public mesh::MainBoard { #endif #else Wire.begin(); - #endif + #endif } // Temperature from ESP32 MCU @@ -63,31 +62,6 @@ class ESP32Board : public mesh::MainBoard { return raw / 4; } - void powerOff() override { - enterDeepSleep(0); // Do not wakeup - } - - void enterDeepSleep(uint32_t secs) { - // Clear stale wakeup sources to avoid ghost wakeup - // This is required when Power Management and automatic lightsleep are enabled - esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000ULL); - } - - // Keep LoRa inactive during deepsleep - digitalWrite(P_LORA_NSS, HIGH); -#if CONFIG_IDF_TARGET_ESP32C3 - gpio_hold_en((gpio_num_t)P_LORA_NSS); -#else - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); -#endif - - // Finally set ESP32 into deepsleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - uint32_t getIRQGpio() override { return P_LORA_DIO_1; // default for SX1262 } @@ -99,15 +73,8 @@ class ESP32Board : public mesh::MainBoard { return; } - #if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT - if (Serial) { - delay(1); - return; - } - #endif - // Set GPIO wakeup - gpio_num_t wakeupPin = (gpio_num_t)getIRQGpio(); + gpio_num_t wakeupPin = (gpio_num_t)getIRQGpio(); // Configure timer wakeup if (secs > 0) { @@ -188,68 +155,21 @@ class ESP32Board : public mesh::MainBoard { void setInhibitSleep(bool inhibit) { inhibit_sleep = inhibit; } - - uint32_t getResetReason() const override { - return esp_reset_reason(); - } - - // https://docs.espressif.com/projects/esp-idf/en/v4.4.7/esp32/api-reference/system/system.html - const char *getResetReasonString(uint32_t reason) { - switch (reason) { - case ESP_RST_UNKNOWN: - return "Unknown or first boot"; - case ESP_RST_POWERON: - return "Power-on reset"; - case ESP_RST_EXT: - return "External reset"; - case ESP_RST_SW: - return "Software reset"; - case ESP_RST_PANIC: - return "Panic / exception reset"; - case ESP_RST_INT_WDT: - return "Interrupt watchdog reset"; - case ESP_RST_TASK_WDT: - return "Task watchdog reset"; - case ESP_RST_WDT: - return "Other watchdog reset"; - case ESP_RST_DEEPSLEEP: - return "Wake from deep sleep"; - case ESP_RST_BROWNOUT: - return "Brownout (low voltage)"; - case ESP_RST_SDIO: - return "SDIO reset"; - default: - static char buf[40]; - snprintf(buf, sizeof(buf), "Unknown reset reason (%d)", reason); - return buf; - } - } }; -static RTC_NOINIT_ATTR uint32_t _rtc_backup_time; -static RTC_NOINIT_ATTR uint32_t _rtc_backup_magic; -#define RTC_BACKUP_MAGIC 0xAA55CC33 -#define RTC_TIME_MIN 1772323200 // 1 Mar 2026 - class ESP32RTCClock : public mesh::RTCClock { public: ESP32RTCClock() { } void begin() { esp_reset_reason_t reason = esp_reset_reason(); - if (reason == ESP_RST_DEEPSLEEP) { - return; // ESP-IDF preserves system time across deep sleep - } - // All other resets (power-on, crash, WDT, brownout) lose system time. - // Restore from RTC backup if valid, otherwise use hardcoded seed. - struct timeval tv; - if (_rtc_backup_magic == RTC_BACKUP_MAGIC && _rtc_backup_time > RTC_TIME_MIN) { - tv.tv_sec = _rtc_backup_time; - } else { - tv.tv_sec = 1772323200; // 1 Mar 2026 - } + if (reason == ESP_RST_POWERON) { + // start with some date/time in the recent past + struct timeval tv; + tv.tv_sec = 1715770351; // 15 May 2024, 8:50pm tv.tv_usec = 0; settimeofday(&tv, NULL); } + } uint32_t getCurrentTime() override { time_t _now; time(&_now); @@ -260,16 +180,6 @@ class ESP32RTCClock : public mesh::RTCClock { tv.tv_sec = time; tv.tv_usec = 0; settimeofday(&tv, NULL); - _rtc_backup_time = time; - _rtc_backup_magic = RTC_BACKUP_MAGIC; - } - void tick() override { - time_t now; - time(&now); - if (now > RTC_TIME_MIN && (uint32_t)now != _rtc_backup_time) { - _rtc_backup_time = (uint32_t)now; - _rtc_backup_magic = RTC_BACKUP_MAGIC; - } } }; diff --git a/src/helpers/MeshadventurerBoard.h b/src/helpers/MeshadventurerBoard.h index 0325161d5d..65e1110294 100644 --- a/src/helpers/MeshadventurerBoard.h +++ b/src/helpers/MeshadventurerBoard.h @@ -15,6 +15,8 @@ #include "ESP32Board.h" +#include + class MeshadventurerBoard : public ESP32Board { public: @@ -33,6 +35,34 @@ class MeshadventurerBoard : public ESP32Board { } } + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are held on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + + void powerOff() override { + // TODO: re-enable this when there is a definite wake-up source pin: + // enterDeepSleep(0); + } + uint16_t getBattMilliVolts() override { analogReadResolution(12); diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 2c8753d464..17265f0455 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -66,20 +66,6 @@ void NRF52Board::initPowerMgr() { } } -bool NRF52Board::isExternalPowered() { - // Check if SoftDevice is enabled before using its API - uint8_t sd_enabled = 0; - sd_softdevice_is_enabled(&sd_enabled); - - if (sd_enabled) { - uint32_t usb_status; - sd_power_usbregstatus_get(&usb_status); - return (usb_status & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0; - } else { - return (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0; - } -} - const char* NRF52Board::getResetReasonString(uint32_t reason) { if (reason & POWER_RESETREAS_RESETPIN_Msk) return "Reset Pin"; if (reason & POWER_RESETREAS_DOG_Msk) return "Watchdog"; @@ -251,6 +237,20 @@ void NRF52BoardDCDC::begin() { } } +bool NRF52Board::isExternalPowered() { + // Check if SoftDevice is enabled before using its API + uint8_t sd_enabled = 0; + sd_softdevice_is_enabled(&sd_enabled); + + if (sd_enabled) { + uint32_t usb_status; + sd_power_usbregstatus_get(&usb_status); + return (usb_status & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0; + } else { + return (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0; + } +} + void NRF52Board::sleep(uint32_t secs) { // Clear FPU interrupt flags to avoid insomnia // see errata 87 for details https://docs.nordicsemi.com/bundle/errata_nRF52840_Rev3/page/ERR/nRF52840/Rev3/latest/anomaly_840_87.html diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 96f67dc950..17065cf443 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -53,9 +53,9 @@ class NRF52Board : public mesh::MainBoard { virtual bool getBootloaderVersion(char* version, size_t max_len) override; virtual bool startOTAUpdate(const char *id, char reply[]) override; virtual void sleep(uint32_t secs) override; + bool isExternalPowered() override; #ifdef NRF52_POWER_MANAGEMENT - bool isExternalPowered() override; uint16_t getBootVoltage() override { return boot_voltage_mv; } virtual uint32_t getResetReason() const override { return reset_reason; } uint8_t getShutdownReason() const override { return shutdown_reason; } @@ -67,7 +67,7 @@ class NRF52Board : public mesh::MainBoard { /* * The NRF52 has an internal DC/DC regulator that allows increased efficiency * compared to the LDO regulator. For being able to use it, the module/board - * needs to have the required inductors and and capacitors populated. If the + * needs to have the required inductors and capacitors populated. If the * hardware requirements are met, this subclass can be used to enable the DC/DC * regulator. */ diff --git a/src/helpers/SensorManager.h b/src/helpers/SensorManager.h index d4aa63b70f..89a174c228 100644 --- a/src/helpers/SensorManager.h +++ b/src/helpers/SensorManager.h @@ -2,7 +2,6 @@ #include #include "sensors/LocationProvider.h" -#include #define TELEM_PERM_BASE 0x01 // 'base' permission includes battery #define TELEM_PERM_LOCATION 0x02 @@ -16,7 +15,6 @@ class SensorManager { double node_altitude; // altitude in meters SensorManager() { node_lat = 0; node_lon = 0; node_altitude = 0; } - virtual bool i2c_probe(TwoWire& wire, uint8_t addr) { return false; } virtual bool begin() { return false; } virtual bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { return false; } virtual void loop() { } diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index 50e1501e5e..dcfa0e1e34 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -182,10 +182,6 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { #define BLE_WRITE_MIN_INTERVAL 60 -bool SerialBLEInterface::isReadBusy() const { - return (recv_queue_len > 0); -} - bool SerialBLEInterface::isWriteBusy() const { return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write? } diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 19e024b040..965e90fd19 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -76,7 +76,6 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE bool isConnected() const override; - bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; diff --git a/src/helpers/esp32/SerialWifiInterface.cpp b/src/helpers/esp32/SerialWifiInterface.cpp index bdecb1a9dd..462e3ecc30 100644 --- a/src/helpers/esp32/SerialWifiInterface.cpp +++ b/src/helpers/esp32/SerialWifiInterface.cpp @@ -39,10 +39,6 @@ size_t SerialWifiInterface::writeFrame(const uint8_t src[], size_t len) { return 0; } -bool SerialWifiInterface::isReadBusy() const { - return false; -} - bool SerialWifiInterface::isWriteBusy() const { return false; } diff --git a/src/helpers/esp32/SerialWifiInterface.h b/src/helpers/esp32/SerialWifiInterface.h index 1ff1d83d25..19291497fe 100644 --- a/src/helpers/esp32/SerialWifiInterface.h +++ b/src/helpers/esp32/SerialWifiInterface.h @@ -52,7 +52,6 @@ class SerialWifiInterface : public BaseSerialInterface { bool isEnabled() const override { return _isEnabled; } bool isConnected() const override; - bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; diff --git a/src/helpers/esp32/TBeamBoard.h b/src/helpers/esp32/TBeamBoard.h index 543226d662..98bd16bff4 100644 --- a/src/helpers/esp32/TBeamBoard.h +++ b/src/helpers/esp32/TBeamBoard.h @@ -86,6 +86,7 @@ #include #include "XPowersLib.h" #include "helpers/ESP32Board.h" +#include class TBeamBoard : public ESP32Board { XPowersLibInterface *PMU = NULL; @@ -130,6 +131,29 @@ bool power_init(); } #endif + void enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! +} + uint16_t getBattMilliVolts(){ return PMU->getBattVoltage(); } diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index a846e744ed..75a4e3b064 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -401,10 +401,6 @@ bool SerialBLEInterface::isConnected() const { return _isDeviceConnected && Bluefruit.connected() > 0; } -bool SerialBLEInterface::isReadBusy() const { - return (recv_queue_len > 0); -} - bool SerialBLEInterface::isWriteBusy() const { return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3); } diff --git a/src/helpers/nrf52/SerialBLEInterface.h b/src/helpers/nrf52/SerialBLEInterface.h index d3cc505516..de1030548f 100644 --- a/src/helpers/nrf52/SerialBLEInterface.h +++ b/src/helpers/nrf52/SerialBLEInterface.h @@ -66,7 +66,6 @@ class SerialBLEInterface : public BaseSerialInterface { void disable() override; bool isEnabled() const override { return _isEnabled; } bool isConnected() const override; - bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index ea57677f25..73842d9eeb 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -15,7 +15,6 @@ #if ENV_INCLUDE_BME680_BSEC #ifndef TELEM_BME680_ADDRESS #define TELEM_BME680_ADDRESS 0x76 -#define TELEM_BME680_ADDRESS_2 0x77 #endif #define TELEM_BME680_SEALEVELPRESSURE_HPA (1013.25) #include @@ -41,7 +40,6 @@ static uint32_t bsec_last_save_ms = 0; #ifdef ENV_INCLUDE_BME680 #ifndef TELEM_BME680_ADDRESS #define TELEM_BME680_ADDRESS 0x76 -#define TELEM_BME680_ADDRESS_2 0x77 #endif #define TELEM_BME680_SEALEVELPRESSURE_HPA (1013.25) #include @@ -65,7 +63,6 @@ static Adafruit_AHTX0 AHTX0; #if ENV_INCLUDE_BME280 #ifndef TELEM_BME280_ADDRESS #define TELEM_BME280_ADDRESS 0x76 // BME280 environmental sensor I2C address -#define TELEM_BME280_ADDRESS_2 0x77 #endif #define TELEM_BME280_SEALEVELPRESSURE_HPA (1013.25) // Atmospheric pressure at sea level #include @@ -75,7 +72,6 @@ static Adafruit_BME280 BME280; #if ENV_INCLUDE_BMP280 #ifndef TELEM_BMP280_ADDRESS #define TELEM_BMP280_ADDRESS 0x76 // BMP280 environmental sensor I2C address -#define TELEM_BMP280_ADDRESS_2 0x77 #endif #define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Atmospheric pressure at sea level #include @@ -561,27 +557,15 @@ static const SensorDef SENSOR_TABLE[] = { #endif #ifdef ENV_INCLUDE_BME680 { TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 }, - #ifdef TELEM_BME680_ADDRESS_2 - { TELEM_BME680_ADDRESS_2, "BME680", init_bme680, query_bme680 }, - #endif #endif #if ENV_INCLUDE_BME680_BSEC { TELEM_BME680_ADDRESS, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec }, - #ifdef TELEM_BME680_ADDRESS_2 - { TELEM_BME680_ADDRESS_2, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec }, - #endif #endif #if ENV_INCLUDE_BME280 { TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 }, - #ifdef TELEM_BME280_ADDRESS_2 - { TELEM_BME280_ADDRESS_2, "BME280", init_bme280, query_bme280 }, - #endif #endif #if ENV_INCLUDE_BMP280 { TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 }, - #ifdef TELEM_BMP280_ADDRESS_2 - { TELEM_BMP280_ADDRESS_2, "BMP280", init_bmp280, query_bmp280 }, - #endif #endif #if ENV_INCLUDE_SHTC3 { 0x70, "SHTC3", init_shtc3, query_shtc3 }, diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h index b91d2dc38e..29147c8967 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -41,7 +41,6 @@ class EnvironmentSensorManager : public SensorManager { #else EnvironmentSensorManager(){}; #endif - bool i2c_probe(TwoWire& wire, uint8_t addr) override; bool begin() override; bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override; #if ENV_INCLUDE_GPS || defined(ENV_INCLUDE_BME680_BSEC) diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index 8983c91148..a6087dd813 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -63,15 +63,6 @@ void ST7735Display::turnOff() { #else digitalWrite(PIN_TFT_LEDA_CTL, LOW); #endif - - // Prevent back-powering to save 2.8 mA - pinMode(PIN_TFT_CS, INPUT); - pinMode(PIN_TFT_DC, INPUT); - pinMode(PIN_TFT_SDA, INPUT); - pinMode(PIN_TFT_SCL, INPUT); - pinMode(PIN_TFT_RST, INPUT); - pinMode(PIN_TFT_LEDA_CTL, INPUT); - _isOn = false; if (_peripher_power) _peripher_power->release(); From 7d8bb1d319488bf64139d93206b01abfed5ea401 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 16:05:23 -0700 Subject: [PATCH 83/94] Keep Halo branch focused on direct retry --- docs/cli_commands.md | 490 ++++++++---------------------- examples/simple_repeater/MyMesh.h | 39 --- examples/simple_repeater/main.cpp | 5 - src/Mesh.cpp | 308 ------------------- src/Mesh.h | 68 ----- 5 files changed, 127 insertions(+), 783 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index d9a225e8e5..5c5ee6705d 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -28,25 +28,12 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Usage:** - `reboot` -**Note:** No reply is sent. - ---- - -### Power-off the node -**Usage:** -- `poweroff`, or -- `shutdown` - -**Note:** No reply is sent. - --- ### Reset the clock and reboot **Usage:** - `clkreboot` -**Note:** No reply is sent. - --- ### Sync the clock with the remote device @@ -128,66 +115,6 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -### Send flood text to `#repeaters` channel - -**Usage:** -- `send text.flood ` - -**Notes:** -- Sends a `PAYLOAD_TYPE_GRP_TXT` flood message using the built-in `#repeaters` channel key. -- Message format is `: `. - ---- - -### View or change automatic low-battery alerts to `#repeaters` - -**Usage:** -- `get battery.alert` -- `set battery.alert ` -- `get battery.alert.low` -- `set battery.alert.low ` -- `get battery.alert.critical` -- `set battery.alert.critical ` - -**Parameters:** -- `state`: `on` (enable) or `off` (disable) -- `percent`: Battery percentage threshold - -**Default:** `off` - -**Default thresholds:** `20` for `battery.alert.low`, `10` for `battery.alert.critical` - -**Notes:** -- When enabled, sends a `#repeaters` flood text warning if voltage is above `1 V` and the battery estimate is below `battery.alert.low`. -- Warnings repeat every `24` hours, or every `12` hours below `battery.alert.critical`. -- `battery.alert.critical` must be lower than `battery.alert.low`. - ---- - -### Get or set recent repeater prefix/SNR -**Usage:** -- `get recent.repeater` -- `get recent.repeater ` -- `get recent.repeater page ` -- `set recent.repeater ` - -**Parameters:** -- `prefix_hex_6`: Exactly 3 bytes of next-hop prefix in hex (6 chars) -- `snr_db`: SNR in dB (supports decimals; stored at x4 precision) -- `page`: 1-based page number - -**Notes:** -- `set` stores or updates the prefix in the recent repeater table. -- Output rows are `prefix,snr` with optional `,l` for locked manual entries. -- Rows are sorted by prefix width (3-byte, 2-byte, 1-byte), then SNR descending. -- A full direct retry failure lowers the stored SNR by `0.25 dB`. -- If a full failure has no row yet, it first seeds the row at the active retry cutoff + `2.5 dB`, then applies the `0.25 dB` penalty. -- Serial CLI page size is fixed at `128` rows; choose page with `get recent.repeater `. -- Over LoRa remote CLI, page size is fixed at `7` rows; choose page with `get recent.repeater `. -- Repeaters can use adjacent entries in this table to short-circuit non-TRACE direct packets when this node appears later in the direct path. - ---- - ## Statistics ### Clear Stats @@ -292,20 +219,6 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -#### View or change the boosted receive gain mode -**Usage:** -- `get radio.rxgain` -- `set radio.rxgain ` - -**Parameters:** -- `state`: `on`|`off` - -**Default:** `off` - -**Note:** Only available on SX1262 and SX1268 based boards. - ---- - #### Change the radio parameters for a set duration **Usage:** - `tempradio ,,,,` @@ -336,7 +249,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -#### View or change this node's rx boosted gain mode (SX12xx only, v1.14.1+) +#### View or change this node's rx boosted gain mode (SX12xx and LR1110, v1.14.1+) **Usage:** - `get radio.rxgain` - `set radio.rxgain ` @@ -469,7 +382,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Note:** `|` characters are translated to newlines -**Note:** Requires firmware 1.12.+ +**Note:** Requires firmware 1.12+ --- @@ -518,18 +431,6 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -#### View or set reboot interval (Repeater and room server) -**Usage:** -- `get reboot.interval` -- `set reboot.interval ` - -**Parameters:** -- `hours`: 0-255. 0 is disabled - -**Default:** `0` (disabled) - ---- - ### Routing #### View or change this node's repeat flag @@ -560,7 +461,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Note:** the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware >= 1.14. This feature was added in firmware 1.14 -**Temporary Note:** adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propogation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are suppored. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes. +**Temporary Note:** adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propagation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are supported. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes. --- @@ -578,7 +479,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Default:** `off` -**Note:** When it is enabled, repeaters will now reject flood packets which look like they are in a loop. This has been happening recently in some meshes when there is just a single 'bad' repeater firmware out there (prob some forked or custom firmware). If the payload is messed with, then forwarded, the same packet ends up causing a packet storm, repeated up to the max 64 hops. This feature was added in firmware 1.14 +**Note:** When it is enabled, repeaters will now reject flood packets which look like they are in a loop. This has been happening recently in some meshes when there is just a single 'bad' repeater firmware out there (probably some forked or custom firmware). If the payload is messed with, then forwarded, the same packet ends up causing a packet storm, repeated up to the max 64 hops. This feature was added in firmware 1.14 **Example:** If preference is `loop.detect minimal`, and a 1-byte path size packet is received, the repeater will see if its own ID/hash is already in the path. If it's already encoded 4 times, it will reject the packet. If the packet uses 2-byte path size, and repeater's own ID/hash is already encoded 2 times, it rejects. If the packet uses 3-byte path size, and the repeater's own ID/hash is already encoded 1 time, it rejects. @@ -594,6 +495,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Default:** `0.5` +**Note:** When multiple nearby repeaters all hear the same flood packet, each waits a random amount of time before retransmitting to avoid simultaneous collisions. This factor scales the size of that random window. Higher values reduce collision risk at the cost of added latency. `0` disables the window entirely. + --- #### View or change the retransmit delay factor for direct traffic @@ -604,116 +507,9 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `value`: Direct transmit delay factor (0-2) -**Default:** `0.3` - -**Note:** Direct retry waits include the same airtime-based randomized delay calculation as direct retransmits, so this factor also controls retry echo windows. - ---- - -#### View or change whether direct retries use the recent repeater SNR gate -**Usage:** -- `get direct.retry.heard` -- `set direct.retry.heard ` - -**Parameters:** -- `state`: `on`|`off` +**Default:** `0.2` -**Default:** `on` - -**Note:** When enabled, the recent repeater table is the only direct retry eligibility gate. Prefixes missing from the table are assumed reachable; prefixes in the table below the active SNR gate are blocked. Neighbor data is not used. - ---- - -#### View or change adaptive coding rate for direct retry packets -**Usage:** -- `get direct.retry.cr` -- `set direct.retry.cr ,,,` -- `set direct.retry.cr off` - -**Parameters:** -- `cr4_min`: SNR in dB where retry packets use `CR4` -- `cr5_min`: SNR in dB where retry packets use `CR5` -- `cr7_min`: SNR in dB where retry packets use `CR7` -- `cr8_max`: SNR in dB where retry packets use `CR8` - -**Default:** `10.0,7.5,2.5,2.5` - -**Note:** DM retry packets use the next-hop SNR from a recent repeater table entry to pick a local transmit coding rate; if no recent entry is available, retry packets use `CR5`. With the default, SNR `10.0 dB` and up uses `CR4`, SNR `7.5 dB` and up uses `CR5`, SNR `2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. `CR6` is never selected. Use `set direct.retry.cr off` to disable adaptive coding-rate overrides. If adaptive selection chooses `CR4`, retries after the third attempt use `CR5`. - ---- - -#### View or change the SNR margin used for direct retry eligibility -**Usage:** -- `get direct.retry.margin` -- `set direct.retry.margin ` - -**Parameters:** -- `value`: Rooftop preset margin in dB above the SF-specific receive floor (minimum `0`, maximum `40`, quarter-dB precision, default `5.0`) - -**Default:** `5.0` - -**Note:** `get direct.retry.margin` returns the active preset's effective margin. The retry gate uses the active SF floor of `SF5=-2.5`, `SF6=-5`, `SF7=-7.5`, `SF8=-10`, `SF9=-12.5`, `SF10=-15`, `SF11=-17.5`, `SF12=-20`, then adds this margin. - ---- - -#### View or change the retry preset -**Usage:** -- `get retry.preset` -- `set retry.preset ` - -**Parameters:** -- `value`: `infra`|`rooftop`|`mobile` or `0`|`1`|`2` - -**Default:** `rooftop` (`1`) - -**Presets:** -- `infra` (`0`): `275 ms` direct base wait, `4` direct retries, `150 ms` added per direct retry, SNR gate is SF floor + `15 dB`; flood retry defaults to `1` retry and path gate `1` -- `rooftop` (`1`): `175 ms` direct base wait, `15` direct retries, `100 ms` added per direct retry, SNR gate is SF floor + `5 dB`; flood retry defaults to `3` retries and path gate `2` -- `mobile` (`2`): `175 ms` direct base wait, `15` direct retries, `50 ms` added per direct retry, SNR gate is the SF floor; flood retry defaults to `3` retries and path gate `1` - -**Note:** Selecting a preset copies those values into the direct retry settings and resets flood retry defaults. You can refine `direct.retry.margin`, `direct.retry.count`, `direct.retry.base`, `direct.retry.step`, `flood.retry.count`, or `flood.retry.path` afterward. Retry delay is `direct.txdelay` jitter + base wait + packet-length airtime wait + per-attempt step. - ---- - -#### View or change the number of direct retry attempts -**Usage:** -- `get direct.retry.count` -- `set direct.retry.count ` - -**Parameters:** -- `value`: Maximum retry attempts after initial TX (`1`-`15`) - -**Default:** `15` - -**Note:** The effective value is capped by total direct path length: paths of `3` hops or less use at most `8` retries, `4` hops use at most `12`, and `5+` hops use at most `15`. A queued resend is canceled early when the next-hop echo is heard. - ---- - -#### View or change the base direct retry wait (milliseconds) -**Usage:** -- `get direct.retry.base` -- `set direct.retry.base ` - -**Parameters:** -- `value`: Base wait in milliseconds (`10`-`5000`) - -**Default:** `175` - -**Note:** The configured base is added to packet-length airtime and `direct.txdelay` jitter. Preset defaults are already reduced to account for the added `direct.txdelay` component. - ---- - -#### View or change the direct retry per-attempt add time (milliseconds) -**Usage:** -- `get direct.retry.step` -- `set direct.retry.step ` - -**Parameters:** -- `value`: Milliseconds added per retry attempt (`0`-`5000`) - -**Default:** `100` - -**Note:** This controls the linear add after the first retry wait. For example, `base=300` and `step=150` adds `0`, `150`, `300`, ... ms across retry attempts. +**Note:** Same collision-avoidance random window as `txdelay`, but applied to direct (non-flood, routed) traffic. The default is lower because direct packets are addressed to a specific next hop, so far fewer nodes compete to retransmit them. --- @@ -727,6 +523,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Default:** `0.0` +**Note:** When enabled, repeaters that received a flood packet with a weak signal are held in a delay queue before processing, while those that received it with a strong signal process it immediately. This gives strong-signal paths forwarding priority. By the time weak-signal nodes process their copy, the packet may have already propagated and will be suppressed as a duplicate, reducing redundant retransmissions. + --- #### View or change the duty cycle limit @@ -854,114 +652,18 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -#### View or change the number of flood retry attempts -**Usage:** -- `get flood.retry.count` -- `set flood.retry.count ` - -**Parameters:** -- `value`: Maximum retry attempts after initial flood TX (`0`-`15`) - -**Default:** `3` for `rooftop` and `mobile`, `1` for `infra` - -**Note:** `0` disables flood retry. - ---- - -#### View or change the flood retry path gate -**Usage:** -- `get flood.retry.path` -- `set flood.retry.path ` - -**Parameters:** -- `value`: Maximum flood path length eligible for retry (`0`-`63`), or `off` to disable the gate - -**Default:** `2` for `rooftop`, `1` for `infra` and `mobile` - -**Note:** Prefixes in `flood.retry.ignore` do not count toward this path length. - ---- - -#### View or change whether advert packets are flood-retried -**Usage:** -- `get flood.retry.advert` -- `set flood.retry.advert ` - -**Parameters:** -- `state`: `on` or `off` - -**Default:** `off` - -**Note:** When this is `off`, node advert packets (`PAYLOAD_TYPE_ADVERT`, type `4`) are not queued for flood retry. - ---- - -#### View or change flood retry target prefixes -**Usage:** -- `get flood.retry.prefixes` -- `set flood.retry.prefixes ` - -**Parameters:** -- `prefixes`: Comma-separated 3-byte hex prefixes, such as `A1B2C3,D4E5F6`; use `none` or `off` to clear - -**Default:** `none` - -**Note:** Prefixes are stored as 3 bytes. Flood retry skips packets whose path already contains a matching target prefix. When prefixes are configured, only a downstream echo from one of those target prefixes cancels a queued retry; when no prefixes are configured, any downstream echo cancels it. Matching works with 3-byte, 2-byte, or 1-byte flood paths by comparing the matching leading bytes. - ---- - -#### View or change flood retry bridge mode +#### Limit the number of hops for an unscoped flood message **Usage:** -- `get flood.retry.bridge` -- `set flood.retry.bridge ` +- `get flood.max.unscoped` +- `set flood.max.unscoped ` **Parameters:** -- `state`: `on` or `off` - -**Default:** `off` - -**Note:** Bridge mode uses bucket definitions instead of the single `flood.retry.prefixes` target list. It also has an implicit unconfigured catch-all bucket. If a flood comes from one fresh configured bucket, retry continues until every other fresh configured bucket plus the catch-all bucket has been heard or `flood.retry.count` is exhausted. If a flood comes from an unconfigured or pathless source, retry targets every fresh configured bucket. This means one configured bucket bridges between that bucket and everything else. Prefixes in `flood.retry.ignore` never count as heard bridge targets. - ---- - -#### View or change flood retry bridge buckets -**Usage:** -- `get flood.retry.bucket.` -- `set flood.retry.bucket ` - -**Parameters:** -- `bucket`: Bucket number (`1`-`6`) -- `prefixes`: Up to 17 comma-separated 3-byte hex prefixes, such as `AABBCC,223344`; use `none` or `off` to clear - -**Default:** all buckets empty - -**Note:** Prefixes are stored as 3 bytes but match 3-byte, 2-byte, and 1-byte flood paths by comparing leading bytes. Bucket prefixes are included in bridge retry logic only if they were heard in the recent repeater table within the last hour. - ---- - -#### View or change flood retry ignored prefixes -**Usage:** -- `get flood.retry.ignore` -- `set flood.retry.ignore ` - -**Parameters:** -- `prefixes`: Up to 8 comma-separated 3-byte hex prefixes, such as `AABBCC,223344`; use `none` or `off` to clear - -**Default:** empty +- `value`: Maximum flood hop count (0-64) for a packet without a scope (no region set) -**Note:** In non-bridge retry, an echo whose last hop matches an ignored prefix does not cancel a queued retry as successful. In bridge mode, ignored prefixes do not count as a heard bridge bucket or as the implicit catch-all bucket when bridge retry decides whether every target has repeated the flood. +**Default:** `0xFF` - indicates it hasn't been set, will track flood.max until it is. ---- - -#### Limit the number of hops for an advert flood message -**Usage:** -- `get flood.max.advert` -- `set flood.max.advert ` - -**Parameters:** -- `value`: Maximum flood hop count (0-64) for an advert packet +**Note:** An alternative to `region denyf *`, setting `flood.max.unscoped` to a lower value such as `3` would allow for local unscoped messages to propagate, while preventing noisy neighbors from flooding a local region. -**Default:** `8` --- @@ -991,32 +693,6 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -#### View or set direct path overrides for the current remote client -**Usage:** -- `get outpath` -- `set outpath ` -- `set outpath direct` -- `set outpath clear` -- `set outpath flood` -- `get altpath` -- `set altpath ` -- `set altpath clear` - -**Parameters:** -- `hopN_hex`: Hop hash, `2`, `4`, or `6` hex characters. All hops must use the same width. - -**Notes:** -- These commands require remote client context; they target the caller's ACL entry. -- The path hash size is inferred from the hop hash width. -- `outpath` overrides the primary direct route used for replies to the caller. -- `direct` sets a zero-hop direct route for a caller reachable without repeaters. -- `clear` forgets the current direct path and allows normal path discovery to repopulate it. -- `flood` forces replies to use flood packets until the client logs in again. -- `altpath` is an optional second direct route used for duplicate response attempts. -- `set altpath clear` removes the duplicate route so only one reply is sent. - ---- - #### View or change this room server's 'read-only' flag **Usage:** - `get allow.read.only` @@ -1104,27 +780,6 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -#### View or set the direct path override for the current remote client -**Usage:** -- `get outpath` -- `set outpath ` -- `set outpath direct` -- `set outpath clear` -- `set outpath flood` - -**Parameters:** -- `hopN_hex`: Hop hash, `2`, `4`, or `6` hex characters. All hops must use the same width. - -**Notes:** -- These commands require remote client context (they target the caller's ACL entry). -- The path hash size is inferred from the hop hash width. -- `outpath` overrides the primary direct route used for replies to the caller. -- `direct` sets a zero-hop direct route for a caller reachable without repeaters. -- `clear` forgets the current direct path and allows normal path discovery to repopulate it. -- `flood` forces replies to use flood packets until the client logs in again. - ---- - #### Create a new region **Usage:** - `region put [parent_name]` @@ -1135,6 +790,47 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### Define region hierarchy (single line) +**Usage:** +- `region def [ ...]` + +**Parameters (tokens):** Space-separated. A logical **cursor** starts at the wildcard `*`. + +- **`name`** — Create `name` as a child of the current cursor (equivalent to `region put name` with the cursor as parent). Cursor moves to `name`. +- **`name|jump`** *(or `name,jump`)* — Create `name` as a child of the current cursor, then move the cursor to `jump` (must already exist on the node, or have been created earlier in this command). `jump` is **not** the parent of `name`; use this form to pop back up and start another branch. + +**Behavior:** Each created region defaults to flood-allowed (same as `region put`). The reply is the resulting region tree (same format as bare `region`); review it before running `region save` to persist. On error, the reply is `Err - ...` and any regions placed before the failure remain on the node, just like a partial chain of `region put`. + +**Existing regions:** `region def` does not clear the existing tree — if a name already exists, its parent is updated to the current cursor; otherwise a new region is created. To start from scratch, `region remove` the unwanted regions first. + +**Limits:** Repeater serial accepts one line up to **160 characters**. For larger trees, split across multiple `region def` commands; the cursor resets to `*` between commands, so lead the next command with `child|ancestor` to reposition. Each token splits at most once on `|` — `region def a|b|c|d` is not a flat-list shorthand; see the flat-list example below. + +**Example — linear chain** (each token becomes a child of the previous): +``` +region def a b c d e +region save +``` + +**Example — branched tree** (equivalent to `region put a`, `region put b a`, `region put c b`, `region put d c`, `region put e b`, `region put f e`): +``` +region def a b c d|b e f +region save +``` + +**Example — error and partial state:** +``` +region def a b c|nope d +``` +The reply is `Err - unknown jump: nope`. `a`, `b`, and `c` were placed before the failure; `d` was not. Run `region` to inspect, then re-run with a corrected jump or repair with `region remove` / `region put`. + +**Example — flat list** (each region a child of `*`). Use `|*` after each token to pop the cursor back to the root before the next token: +``` +region def a|* b|* c|* d|* e|* f +region save +``` + +--- + #### Remove a region **Usage:** - `region remove ` @@ -1155,7 +851,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `filter`: `allowed`|`denied` -**Note:** Requires firmware 1.12.+ +**Note:** Requires firmware 1.12+ --- @@ -1312,7 +1008,7 @@ region save --- -#### View or change thevalue of a sensor +#### View or change the value of a sensor **Usage:** - `sensor get ` - `sensor set ` @@ -1432,3 +1128,71 @@ region save **Note:** Returns an error on boards without power management support. --- + +--- + +## Halo Direct Retry Commands + +These commands are available on the Halo direct-message retry branch. See `docs/halo_settings.md` for operating guidance and examples. + +### View or change the retry preset +**Usage:** +- `get retry.preset` +- `set retry.preset ` + +**Parameters:** +- `value`: `infra`|`rooftop`|`mobile` or `0`|`1`|`2` + +--- + +### View or change whether direct retries use the recent repeater SNR gate +**Usage:** +- `get direct.retry.heard` +- `set direct.retry.heard ` + +**Parameters:** +- `state`: `on`|`off` + +--- + +### View or change direct retry timing and count +**Usage:** +- `get direct.retry.margin` +- `set direct.retry.margin ` +- `get direct.retry.count` +- `set direct.retry.count <1-15>` +- `get direct.retry.base` +- `set direct.retry.base <10-5000>` +- `get direct.retry.step` +- `set direct.retry.step <0-5000>` + +--- + +### View or change adaptive coding rate for direct retry packets +**Usage:** +- `get direct.retry.cr` +- `set direct.retry.cr ,,,` +- `set direct.retry.cr off` + +--- + +### Get or set recent repeater prefix/SNR +**Usage:** +- `get recent.repeater` +- `get recent.repeater ` +- `get recent.repeater page ` +- `set recent.repeater ` +- `clear recent.repeater` + +--- + +### View or change direct reply path overrides +**Usage:** +- `get outpath` +- `set outpath ` +- `set outpath direct` +- `set outpath clear` +- `set outpath flood` +- `get altpath` +- `set altpath ` +- `set altpath clear` diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 36c66c3f61..bb39c077c4 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -85,9 +85,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t last_millis; uint64_t uptime_millis; unsigned long next_local_advert, next_flood_advert; - unsigned long next_battery_alert_check; - unsigned long last_battery_alert_sent; - bool battery_alert_sent; bool _logging; NodePrefs _prefs; ClientACL acl; @@ -102,15 +99,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { RegionEntry* recv_pkt_region; TransportKey default_scope; RateLimiter discover_limiter, anon_limiter; - struct FloodRetryBridgeState { - uint8_t key[MAX_HASH_SIZE]; - uint8_t source_bucket; - uint8_t target_mask; - uint8_t heard_mask; - uint8_t progress_marker; - bool active; - }; - mutable FloodRetryBridgeState flood_retry_bridge_states[MAX_FLOOD_RETRY_SLOTS]; uint32_t pending_discover_tag; unsigned long pending_discover_until; bool region_load_active; @@ -140,25 +128,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t getDirectRetryPreset() const; uint8_t getDirectRetryConfiguredMaxAttempts() const; uint32_t getDirectRetryAttemptStepMillis() const; - bool hasFloodRetryPrefixes() const; - bool floodRetryPrefixMatches(const mesh::Packet* packet) const; - bool floodRetryLastHopMatches(const mesh::Packet* packet) const; - bool floodRetryPrefixIgnored(const uint8_t* prefix, uint8_t prefix_len) const; - uint8_t floodRetryEffectivePathLength(const mesh::Packet* packet, uint8_t max_hops = 0xFF) const; - bool floodRetryPrefixFresh(const uint8_t* prefix, uint8_t prefix_len) const; - int floodRetryBucketForPrefix(const uint8_t* prefix, uint8_t prefix_len, bool require_fresh, - bool include_other) const; - int floodRetryBucketForPathHop(const uint8_t* prefix, uint8_t prefix_len, uint8_t hop, - uint8_t progress_marker) const; - int floodRetrySourceBucket(const mesh::Packet* packet) const; - uint8_t floodRetryBridgeTargetMask(uint8_t source_bucket) const; - uint8_t floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t source_bucket, - uint8_t progress_marker) const; - FloodRetryBridgeState* floodRetryBridgeStateFor(const mesh::Packet* packet, bool create) const; - void clearFloodRetryBridgeState(const mesh::Packet* packet); - void refreshFloodRetryHeardRecent(const mesh::Packet* packet); - void formatFloodRetryPath(char* dest, size_t dest_len, const mesh::Packet* packet) const; - bool formatFloodRetryHeard(char* dest, size_t dest_len, const mesh::Packet* packet) const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); @@ -166,8 +135,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); - bool sendRepeatersFloodText(const char* text); - void checkBatteryAlert(); File openAppend(const char* fname); bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]); @@ -196,12 +163,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override; uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override; void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override; - bool allowFloodRetry(const mesh::Packet* packet) const override; - void onFloodRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override; - bool hasFloodRetryTargetPrefix(const mesh::Packet* packet) const override; - uint8_t getFloodRetryMaxPathLength(const mesh::Packet* packet) const override; - uint8_t getFloodRetryMaxAttempts(const mesh::Packet* packet) const override; - bool isFloodRetryEchoTarget(const mesh::Packet* packet, uint8_t progress_marker) const override; int getInterferenceThreshold() const override { return _prefs.interference_threshold; diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index b9b1ab4d88..03a89dd46a 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -159,9 +159,4 @@ void loop() { } #endif } - - if (the_mesh.getNodePrefs()->reboot_interval > 0 && - the_mesh.millisHasNowPassed(the_mesh.getNodePrefs()->reboot_interval * 3600000)) { - board.reboot(); - } } diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 4dccd45acf..a9f7aac0c5 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -5,8 +5,6 @@ namespace mesh { static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 15; static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; -static const uint8_t FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT = 3; -static const uint8_t FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) { uint8_t code = flags & 0x03; @@ -44,19 +42,6 @@ void Mesh::begin() { _direct_retries[i].queued = false; _direct_retries[i].active = false; } - for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { - _flood_retries[i].packet = NULL; - _flood_retries[i].trigger_packet = NULL; - _flood_retries[i].retry_started_at = 0; - _flood_retries[i].retry_at = 0; - _flood_retries[i].retry_delay = 0; - _flood_retries[i].retry_attempts_sent = 0; - _flood_retries[i].priority = 0; - _flood_retries[i].progress_marker = 0; - _flood_retries[i].waiting_final_echo = false; - _flood_retries[i].queued = false; - _flood_retries[i].active = false; - } Dispatcher::begin(); } @@ -93,37 +78,6 @@ void Mesh::loop() { clearDirectRetrySlot(i); } } - - for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { - if (!_flood_retries[i].active) { - continue; - } - - if (_flood_retries[i].waiting_final_echo) { - if (!millisHasNowPassed(_flood_retries[i].retry_at)) { - continue; - } - - uint32_t elapsed_millis = _flood_retries[i].retry_started_at == 0 - ? 0 - : (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at); - onFloodRetryEvent("failed_all_tries", _flood_retries[i].packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); - onFloodRetryEvent("failure", _flood_retries[i].packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); - clearFloodRetrySlot(i); - continue; - } - - if (!_flood_retries[i].queued || !millisHasNowPassed(_flood_retries[i].retry_at)) { - continue; - } - - if (!isFloodRetryQueued(_flood_retries[i].packet)) { - if (_flood_retries[i].packet == getOutboundInFlight()) { - continue; - } - clearFloodRetrySlot(i); - } - } } bool Mesh::allowPacketForward(const mesh::Packet* packet) { @@ -152,39 +106,16 @@ uint32_t Mesh::getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_ // Keep the historical linear spacing while allowing the base wait to vary by platform/profile. return base + ((uint32_t)attempt_idx * 100UL); } -bool Mesh::allowFloodRetry(const Packet* packet) const { - return true; -} -bool Mesh::hasFloodRetryTargetPrefix(const Packet* packet) const { - return false; -} -uint8_t Mesh::getFloodRetryMaxPathLength(const Packet* packet) const { - return 2; -} -uint8_t Mesh::getFloodRetryMaxAttempts(const Packet* packet) const { - return FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT; -} -uint32_t Mesh::getFloodRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) { - if (packet == NULL) { - return _radio->getEstAirtimeFor(MAX_TRANS_UNIT); - } - - uint32_t max_packet_airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT); - uint32_t packet_airtime = _radio->getEstAirtimeFor(packet->getRawLength()); - return max_packet_airtime + (20UL * packet_airtime); -} uint8_t Mesh::getExtraAckTransmitCount() const { return 0; } void Mesh::onSendComplete(Packet* packet) { armDirectRetryOnSendComplete(packet); - armFloodRetryOnSendComplete(packet); } void Mesh::onSendFail(Packet* packet) { clearPendingDirectRetryOnSendFail(packet); - clearPendingFloodRetryOnSendFail(packet); } uint32_t Mesh::getCADFailRetryDelay() const { @@ -202,8 +133,6 @@ int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int DispatcherAction Mesh::onRecvPacket(Packet* pkt) { if (pkt->isRouteDirect()) { cancelDirectRetryOnEcho(pkt); - } else if (pkt->isRouteFlood()) { - cancelFloodRetryOnEcho(pkt); } if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { @@ -523,7 +452,6 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) { uint32_t d = getRetransmitDelay(packet); uint8_t priority = packet->getPathHashCount(); - maybeScheduleFloodRetry(packet, priority); // as this propagates outwards, give it lower and lower priority return ACTION_RETRANSMIT_DELAYED(priority, d); // give priority to closer sources, than ones further away } @@ -952,242 +880,6 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { _direct_retries[slot_idx].active = true; } -void Mesh::clearFloodRetrySlot(int idx) { - if (_flood_retries[idx].waiting_final_echo && _flood_retries[idx].packet != NULL) { - releasePacket(_flood_retries[idx].packet); - } - _flood_retries[idx].packet = NULL; - _flood_retries[idx].trigger_packet = NULL; - _flood_retries[idx].retry_started_at = 0; - _flood_retries[idx].retry_at = 0; - _flood_retries[idx].retry_delay = 0; - _flood_retries[idx].retry_attempts_sent = 0; - _flood_retries[idx].priority = 0; - _flood_retries[idx].progress_marker = 0; - _flood_retries[idx].waiting_final_echo = false; - _flood_retries[idx].queued = false; - _flood_retries[idx].active = false; -} - -bool Mesh::isFloodRetryQueued(const Packet* packet) const { - for (int i = 0; i < _mgr->getOutboundTotal(); i++) { - if (_mgr->getOutboundByIdx(i) == packet) { - return true; - } - } - return false; -} - -bool Mesh::isFloodRetryEchoTarget(const Packet* packet, uint8_t progress_marker) const { - return packet->isRouteFlood() && packet->getPathHashCount() > progress_marker; -} - -bool Mesh::cancelFloodRetryOnEcho(const Packet* packet) { - uint8_t recv_key[MAX_HASH_SIZE]; - packet->calculatePacketHash(recv_key); - - bool cleared = false; - for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { - if (!_flood_retries[i].active || memcmp(recv_key, _flood_retries[i].retry_key, MAX_HASH_SIZE) != 0) { - continue; - } - if (!isFloodRetryEchoTarget(packet, _flood_retries[i].progress_marker)) { - continue; - } - - uint32_t echo_millis = _flood_retries[i].retry_started_at == 0 - ? 0 - : (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at); - uint8_t retry_attempt = _flood_retries[i].waiting_final_echo - ? _flood_retries[i].retry_attempts_sent - : _flood_retries[i].retry_attempts_sent + 1; - onFloodRetryEvent("good", packet, echo_millis, retry_attempt); - - if (_flood_retries[i].queued) { - for (int j = 0; j < _mgr->getOutboundTotal(); j++) { - if (_mgr->getOutboundByIdx(j) == _flood_retries[i].packet) { - Packet* pending = _mgr->removeOutboundByIdx(j); - if (pending) { - releasePacket(pending); - } - break; - } - } - } - clearFloodRetrySlot(i); - cleared = true; - } - - return cleared; -} - -void Mesh::armFloodRetryOnSendComplete(const Packet* packet) { - for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { - if (!_flood_retries[i].active) { - continue; - } - - if (_flood_retries[i].queued) { - if (_flood_retries[i].packet != packet) { - continue; - } - - uint32_t elapsed_millis = _flood_retries[i].retry_started_at == 0 - ? 0 - : (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at); - onFloodRetryEvent("resent", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); - _flood_retries[i].retry_attempts_sent++; - - uint8_t max_attempts = getFloodRetryMaxAttempts(packet); - if (max_attempts < 1) { - max_attempts = 1; - } else if (max_attempts > FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX) { - max_attempts = FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX; - } - if (_flood_retries[i].retry_attempts_sent >= max_attempts) { - Packet* final_wait = obtainNewPacket(); - if (final_wait == NULL) { - onFloodRetryEvent("dropped_no_packet", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); - onFloodRetryEvent("failure", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); - clearFloodRetrySlot(i); - continue; - } - - *final_wait = *packet; - _flood_retries[i].packet = final_wait; - _flood_retries[i].retry_at = futureMillis(_flood_retries[i].retry_delay); - _flood_retries[i].waiting_final_echo = true; - _flood_retries[i].queued = false; - continue; - } - - Packet* retry = obtainNewPacket(); - if (retry == NULL) { - onFloodRetryEvent("dropped_no_packet", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); - onFloodRetryEvent("failure", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); - clearFloodRetrySlot(i); - continue; - } - - *retry = *packet; - uint32_t retry_delay = getFloodRetryAttemptDelay(packet, _flood_retries[i].retry_attempts_sent); - if (queueOutboundPacket(retry, _flood_retries[i].priority, retry_delay)) { - _flood_retries[i].packet = retry; - _flood_retries[i].retry_delay = retry_delay; - _flood_retries[i].retry_at = futureMillis(retry_delay); - _flood_retries[i].retry_started_at = _ms->getMillis(); - _flood_retries[i].waiting_final_echo = false; - onFloodRetryEvent("queued", retry, retry_delay, _flood_retries[i].retry_attempts_sent + 1); - } else { - onFloodRetryEvent("dropped_queue_full", retry, retry_delay, _flood_retries[i].retry_attempts_sent + 1); - onFloodRetryEvent("failure", retry, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); - releasePacket(retry); - clearFloodRetrySlot(i); - } - continue; - } - - if (_flood_retries[i].trigger_packet != packet) { - continue; - } - - Packet* retry = obtainNewPacket(); - if (retry == NULL) { - onFloodRetryEvent("dropped_no_packet", packet, _flood_retries[i].retry_delay, 1); - onFloodRetryEvent("failure", packet, 0, 1); - clearFloodRetrySlot(i); - continue; - } - - *retry = *packet; - if (queueOutboundPacket(retry, _flood_retries[i].priority, _flood_retries[i].retry_delay)) { - unsigned long now = _ms->getMillis(); - _flood_retries[i].packet = retry; - _flood_retries[i].trigger_packet = NULL; - _flood_retries[i].queued = true; - _flood_retries[i].waiting_final_echo = false; - _flood_retries[i].retry_at = futureMillis(_flood_retries[i].retry_delay); - _flood_retries[i].retry_started_at = now; - onFloodRetryEvent("queued", retry, _flood_retries[i].retry_delay, 1); - } else { - onFloodRetryEvent("dropped_queue_full", retry, _flood_retries[i].retry_delay, 1); - onFloodRetryEvent("failure", retry, 0, 1); - releasePacket(retry); - clearFloodRetrySlot(i); - } - } -} - -void Mesh::clearPendingFloodRetryOnSendFail(const Packet* packet) { - for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { - if (!_flood_retries[i].active) { - continue; - } - - if (_flood_retries[i].queued) { - if (_flood_retries[i].packet == packet) { - onFloodRetryEvent("dropped_send_fail", packet, 0, _flood_retries[i].retry_attempts_sent + 1); - onFloodRetryEvent("failure", packet, 0, _flood_retries[i].retry_attempts_sent + 1); - clearFloodRetrySlot(i); - } - continue; - } - - if (_flood_retries[i].trigger_packet == packet) { - onFloodRetryEvent("dropped_send_fail", packet, 0, 1); - onFloodRetryEvent("failure", packet, 0, 1); - clearFloodRetrySlot(i); - } - } -} - -void Mesh::maybeScheduleFloodRetry(const Packet* packet, uint8_t priority) { - if (packet == NULL || !packet->isRouteFlood() || hasFloodRetryTargetPrefix(packet)) { - return; - } - - uint8_t max_path_len = getFloodRetryMaxPathLength(packet); - if (max_path_len != FLOOD_RETRY_PATH_GATE_DISABLED && packet->getPathHashCount() > max_path_len) { - return; - } - - uint8_t max_attempts = getFloodRetryMaxAttempts(packet); - if (max_attempts == 0) { - return; - } - - int slot_idx = -1; - for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { - if (!_flood_retries[i].active) { - slot_idx = i; - break; - } - } - if (slot_idx < 0) { - onFloodRetryEvent("dropped_no_slot", packet, 0, 0); - onFloodRetryEvent("failure", packet, 0, 0); - return; - } - - if (!allowFloodRetry(packet)) { - return; - } - - uint32_t retry_delay = getFloodRetryAttemptDelay(packet, 0); - packet->calculatePacketHash(_flood_retries[slot_idx].retry_key); - _flood_retries[slot_idx].packet = NULL; - _flood_retries[slot_idx].trigger_packet = const_cast(packet); - _flood_retries[slot_idx].retry_started_at = 0; - _flood_retries[slot_idx].retry_at = 0; - _flood_retries[slot_idx].retry_delay = retry_delay; - _flood_retries[slot_idx].retry_attempts_sent = 0; - _flood_retries[slot_idx].priority = priority; - _flood_retries[slot_idx].progress_marker = packet->getPathHashCount(); - _flood_retries[slot_idx].waiting_final_echo = false; - _flood_retries[slot_idx].queued = false; - _flood_retries[slot_idx].active = true; -} - Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) { if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL; diff --git a/src/Mesh.h b/src/Mesh.h index 6ae26642fc..314db23722 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -8,14 +8,6 @@ namespace mesh { #define MAX_DIRECT_RETRY_SLOTS 6 #endif -#ifndef MAX_FLOOD_RETRY_SLOTS - #define MAX_FLOOD_RETRY_SLOTS 6 -#endif - -#ifndef FLOOD_RETRY_PATH_GATE_DISABLED - #define FLOOD_RETRY_PATH_GATE_DISABLED 0xFF -#endif - class GroupChannel { public: uint8_t hash[PATH_HASH_SIZE]; @@ -54,26 +46,7 @@ class Mesh : public Dispatcher { bool active; }; - struct FloodRetryEntry { - Packet* packet; - Packet* trigger_packet; - unsigned long retry_started_at; - unsigned long retry_at; - uint32_t retry_delay; - uint8_t retry_attempts_sent; - uint8_t retry_key[MAX_HASH_SIZE]; - uint8_t priority; - uint8_t progress_marker; - bool waiting_final_echo; - bool queued; - bool active; - }; - - RTCClock* _rtc; - RNG* _rng; - MeshTables* _tables; DirectRetryEntry _direct_retries[MAX_DIRECT_RETRY_SLOTS]; - FloodRetryEntry _flood_retries[MAX_FLOOD_RETRY_SLOTS]; void removePathPrefix(Packet* packet, uint8_t prefix_count); void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis); @@ -87,12 +60,6 @@ class Mesh : public Dispatcher { uint8_t& progress_marker, bool& expect_path_growth) const; bool canDecodeDirectPayloadForSelf(const Packet* packet); void maybeScheduleDirectRetry(const Packet* packet, uint8_t priority); - void clearFloodRetrySlot(int idx); - bool isFloodRetryQueued(const Packet* packet) const; - bool cancelFloodRetryOnEcho(const Packet* packet); - void armFloodRetryOnSendComplete(const Packet* packet); - void clearPendingFloodRetryOnSendFail(const Packet* packet); - void maybeScheduleFloodRetry(const Packet* packet, uint8_t priority); //void routeRecvAcks(Packet* packet, uint32_t delay_millis); DispatcherAction forwardMultipartDirect(Packet* pkt); @@ -156,41 +123,6 @@ class Mesh : public Dispatcher { */ virtual uint32_t getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx); - /** - * \brief Decide whether a FLOOD packet should retry when no downstream echo is overheard. - */ - virtual bool allowFloodRetry(const Packet* packet) const; - - /** - * \brief Return true when this FLOOD packet already carries an application-defined target prefix. - */ - virtual bool hasFloodRetryTargetPrefix(const Packet* packet) const; - - /** - * \returns maximum flood path hash count eligible for retry, or FLOOD_RETRY_PATH_GATE_DISABLED. - */ - virtual uint8_t getFloodRetryMaxPathLength(const Packet* packet) const; - - /** - * \returns maximum number of FLOOD retry transmissions after the initial TX. - */ - virtual uint8_t getFloodRetryMaxAttempts(const Packet* packet) const; - - /** - * \brief Return true when a received FLOOD echo is enough to cancel a pending retry. - */ - virtual bool isFloodRetryEchoTarget(const Packet* packet, uint8_t progress_marker) const; - - /** - * \returns delay before a specific flood retry attempt, where attempt_idx=0 is the first retry. - */ - virtual uint32_t getFloodRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx); - - /** - * \brief Optional hook for logging flood-retry lifecycle events. - */ - virtual void onFloodRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { } - /** * \returns number of extra (Direct) ACK transmissions wanted. */ From 765ec1447badc7b6efdbfeaa19a7449361f08bbb Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 17:24:22 -0700 Subject: [PATCH 84/94] Add direct retry controls --- docs/cli_commands.md | 68 ------- docs/halo_settings.md | 136 -------------- examples/simple_repeater/MyMesh.cpp | 226 +++++++++++++++++++++++ examples/simple_repeater/MyMesh.h | 24 +-- examples/simple_repeater/main.cpp | 6 +- src/Mesh.cpp | 132 +++++-------- src/Mesh.h | 19 +- src/helpers/ClientACL.cpp | 13 +- src/helpers/ClientACL.h | 5 +- src/helpers/CommonCLI.cpp | 277 +++++++++++++++++++++++++++- src/helpers/CommonCLI.h | 51 +++++ src/helpers/SimpleMeshTables.h | 110 +++-------- 12 files changed, 650 insertions(+), 417 deletions(-) delete mode 100644 docs/halo_settings.md diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 5c5ee6705d..c06f5e12b3 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1128,71 +1128,3 @@ region save **Note:** Returns an error on boards without power management support. --- - ---- - -## Halo Direct Retry Commands - -These commands are available on the Halo direct-message retry branch. See `docs/halo_settings.md` for operating guidance and examples. - -### View or change the retry preset -**Usage:** -- `get retry.preset` -- `set retry.preset ` - -**Parameters:** -- `value`: `infra`|`rooftop`|`mobile` or `0`|`1`|`2` - ---- - -### View or change whether direct retries use the recent repeater SNR gate -**Usage:** -- `get direct.retry.heard` -- `set direct.retry.heard ` - -**Parameters:** -- `state`: `on`|`off` - ---- - -### View or change direct retry timing and count -**Usage:** -- `get direct.retry.margin` -- `set direct.retry.margin ` -- `get direct.retry.count` -- `set direct.retry.count <1-15>` -- `get direct.retry.base` -- `set direct.retry.base <10-5000>` -- `get direct.retry.step` -- `set direct.retry.step <0-5000>` - ---- - -### View or change adaptive coding rate for direct retry packets -**Usage:** -- `get direct.retry.cr` -- `set direct.retry.cr ,,,` -- `set direct.retry.cr off` - ---- - -### Get or set recent repeater prefix/SNR -**Usage:** -- `get recent.repeater` -- `get recent.repeater ` -- `get recent.repeater page ` -- `set recent.repeater ` -- `clear recent.repeater` - ---- - -### View or change direct reply path overrides -**Usage:** -- `get outpath` -- `set outpath ` -- `set outpath direct` -- `set outpath clear` -- `set outpath flood` -- `get altpath` -- `set altpath ` -- `set altpath clear` diff --git a/docs/halo_settings.md b/docs/halo_settings.md deleted file mode 100644 index 9324487d29..0000000000 --- a/docs/halo_settings.md +++ /dev/null @@ -1,136 +0,0 @@ -# Halo Direct Message Retry Settings - -This file covers only CLI settings and helper commands added for Halo direct-message retry behavior. Use `docs/cli_commands.md` for the general MeshCore CLI. - -Halo retry applies to direct-routed packets. A queued resend is canceled when the next-hop echo is heard. - -## Quick Start - -```text -set retry.preset rooftop -set direct.retry.heard on -get retry.preset -get direct.retry.heard -get direct.retry.count -get direct.retry.base -get direct.retry.step -``` - -Use prefixes from the analyzer, neighbors list, or `get recent.repeater` after the repeater has been online for a few hours. - -## Added Halo Settings - -| Setting | What it does | How to use | Example | -| --- | --- | --- | --- | -| `recent.repeater` | Shows, seeds, or clears the recent repeater prefix/SNR table used by direct retry. | `get recent.repeater`, `get recent.repeater `, `set recent.repeater `, `clear recent.repeater` | `set recent.repeater A1B2C3 -8.5` | -| `outpath` | Overrides the primary direct route used for replies to the current remote client. | `get outpath`, `set outpath `, `set outpath direct`, `set outpath clear`, `set outpath flood` | `set outpath A1B2C3,D4E5F6` | -| `altpath` | Optional second direct route used for duplicate response attempts to the current remote client. | `get altpath`, `set altpath `, `set altpath clear` | `set altpath A1B2C3,D4E5F6` | -| `retry.preset` | Applies direct retry defaults. Values: `infra`, `rooftop`, `mobile` or `0`, `1`, `2`. | `get retry.preset`, `set retry.preset ` | `set retry.preset rooftop` | -| `direct.retry.heard` | Uses the recent repeater table as the direct retry eligibility gate. | `get direct.retry.heard`, `set direct.retry.heard on/off` | `set direct.retry.heard on` | -| `direct.retry.margin` | SNR margin in dB above the SF-specific receive floor. | `get direct.retry.margin`, `set direct.retry.margin <0-40>` | `set direct.retry.margin 5` | -| `direct.retry.count` | Maximum direct retry attempts after initial TX. | `get direct.retry.count`, `set direct.retry.count <1-15>` | `set direct.retry.count 15` | -| `direct.retry.base` | Base wait in milliseconds before retry. | `get direct.retry.base`, `set direct.retry.base <10-5000>` | `set direct.retry.base 175` | -| `direct.retry.step` | Milliseconds added per retry attempt. | `get direct.retry.step`, `set direct.retry.step <0-5000>` | `set direct.retry.step 100` | -| `direct.retry.cr` | Adaptive coding-rate thresholds for direct retry packets. Uses `CR4`, `CR5`, `CR7`, or `CR8`; `CR6` is never selected. | `get direct.retry.cr`, `set direct.retry.cr ,,,`, `set direct.retry.cr off` | `set direct.retry.cr 10.0,7.5,2.5,0` | - -## Recent Repeater Table - -Direct retry uses the recent repeater table when `direct.retry.heard` is `on`. - -Show learned rows: - -```text -get recent.repeater -get recent.repeater 2 -get recent.repeater page 3 -``` - -Seed or correct a prefix: - -```text -set recent.repeater A1B2C3 8.5 -``` - -Clear learned and manually seeded rows: - -```text -clear recent.repeater -``` - -Rows are sorted by prefix width, then SNR. A full direct retry failure lowers the matching row by `0.25 dB`. - -Serial CLI pages contain up to `128` rows. Remote LoRa CLI pages contain up to `7` rows. - -## Direct Path Overrides - -`outpath` and `altpath` apply to the current remote client ACL entry. They need remote client context, so they are not useful from the local serial CLI. - -Set paths with comma-separated hop hashes. Each hop must be `2`, `4`, or `6` hex characters, and all hops in one path must use the same width. - -```text -get outpath -set outpath A1B2C3,D4E5F6 -set outpath direct -set outpath clear -set outpath flood - -get altpath -set altpath A1B2C3,D4E5F6 -set altpath clear -``` - -`set outpath direct` sets a zero-hop direct route for a client reachable without repeaters. `set outpath clear` forgets the override and lets normal path discovery fill it again. `set outpath flood` forces replies to use flood packets until the client logs in again. `altpath` sends a duplicate reply over a second direct route; clearing it returns replies to a single route. - -## Direct Retry Details - -The default adaptive coding-rate profile is `10.0,7.5,2.5,2.5`. SNR `10.0 dB` and up uses `CR4`, `7.5 dB` and up uses `CR5`, `2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. If no recent repeater table entry is available, retry packets use `CR5`. - -Use `set direct.retry.cr off` to disable adaptive coding-rate overrides. If adaptive selection chooses `CR4`, retries after the third attempt use `CR5`. - -Preset details: - -| Preset | Base | Count | Step | SNR gate | -| --- | ---: | ---: | ---: | --- | -| `infra` | `275 ms` | `4` | `150 ms` | SF floor + `15 dB` | -| `rooftop` | `175 ms` | `15` | `100 ms` | SF floor + `5 dB` | -| `mobile` | `175 ms` | `15` | `50 ms` | SF floor | - -Example for a quiet fixed repeater: - -```text -set retry.preset rooftop -set direct.retry.heard on -set direct.retry.margin 5 -``` - -Example for a moving or weak-link node: - -```text -set retry.preset mobile -set direct.retry.margin 0 -``` - -## Troubleshooting - -If direct retries are too aggressive: - -```text -set direct.retry.count 4 -set direct.retry.margin 10 -``` - -If direct retries are too sparse: - -```text -set direct.retry.count 15 -set direct.retry.margin 0 -``` - -If direct retry is skipping a path you expect it to retry: - -```text -get direct.retry.heard -get recent.repeater -``` - -Either disable the heard gate with `set direct.retry.heard off`, or seed the next-hop prefix with `set recent.repeater `. diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 5cc3a9a11e..45f81ed241 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -549,6 +549,208 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { return getRNG()->nextInt(0, 5*t + 1); } +bool MyMesh::extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { + if (packet == NULL || !packet->isRouteDirect() || packet->getPathHashCount() == 0) { + return false; + } + prefix_len = packet->getPathHashSize(); + memcpy(prefix, packet->path, prefix_len); + return true; +} + +int8_t MyMesh::getDirectRetryMinSNRX4() const { + switch (active_sf) { + case 7: return -30; + case 8: return -40; + case 9: return -50; + case 10: return -60; + case 11: return -70; + case 12: return -80; + default: return -60; + } +} + +uint8_t MyMesh::getDirectRetryCodingRateForSNR(int8_t snr_x4) const { + if (!_prefs.direct_retry_cr_enabled) return 0; + if (snr_x4 >= _prefs.direct_retry_cr4_snr_x4) return 4; + if (snr_x4 >= _prefs.direct_retry_cr5_snr_x4) return 5; + if (snr_x4 >= _prefs.direct_retry_cr7_snr_x4) return 7; + return 8; +} + +uint8_t MyMesh::getDirectRetryPreset() const { + return _prefs.retry_preset; +} + +uint8_t MyMesh::getDirectRetryConfiguredMaxAttempts() const { + return constrain(_prefs.direct_retry_attempts, 1, 15); +} + +uint32_t MyMesh::getDirectRetryAttemptStepMillis() const { + return _prefs.direct_retry_step_ms; +} + +bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { + (void)packet; + if (next_hop_hash == NULL || next_hop_hash_len == 0) { + return true; + } + const SimpleMeshTables* tables = static_cast(getTables()); + const SimpleMeshTables::RecentRepeaterInfo* repeater = tables != NULL + ? tables->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len) + : NULL; + + if (repeater == NULL) { + // Retry unknown repeaters too. If they fail, onDirectRetryFailed() seeds the + // recent-repeater table below the +2.00 dB starting point. + return true; + } + int16_t retry_floor_x4 = (int16_t)getDirectRetryMinSNRX4() + (int16_t)_prefs.direct_retry_snr_margin_x4; + return (int16_t)repeater->snr_x4 >= retry_floor_x4; +} + +void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) { + (void)retry_attempt; + int8_t snr_x4 = 8; // unknown repeaters start at +2.00 dB + const SimpleMeshTables* tables = static_cast(getTables()); + if (tables != NULL) { + uint8_t prefix[MAX_HASH_SIZE]; + uint8_t prefix_len = 0; + if (extractDirectRetryPrefix(original, prefix, prefix_len)) { + const SimpleMeshTables::RecentRepeaterInfo* repeater = tables->findRecentRepeaterByHash(prefix, prefix_len); + if (repeater != NULL) { + snr_x4 = repeater->snr_x4; + } + } + } + + retry->tx_cr = getDirectRetryCodingRateForSNR(snr_x4); +} + +bool MyMesh::maybeShortCircuitDirect(mesh::Packet* packet) { + (void)packet; + return false; +} + +uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { + (void)packet; + return 200; +} + +uint8_t MyMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const { + (void)packet; + return getDirectRetryConfiguredMaxAttempts(); +} + +uint32_t MyMesh::getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) { + (void)packet; + return _prefs.direct_retry_base_ms + ((uint32_t)attempt_idx * getDirectRetryAttemptStepMillis()); +} + +void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { +#if MESH_DEBUG + MESH_DEBUG_PRINTLN("direct retry %s attempt=%u delay=%lu type=%u route=%s", + event ? event : "?", + (uint32_t)retry_attempt, + (unsigned long)delay_millis, + packet ? (uint32_t)packet->getPayloadType() : 0, + packet && packet->isRouteDirect() ? "D" : "F"); +#endif + if (_logging) { + File f = openAppend(PACKET_LOG_FILE); + if (f) { + f.print(getLogDateTime()); + f.printf(": direct retry %s attempt=%u delay=%lu type=%u route=%s\n", + event ? event : "?", + (uint32_t)retry_attempt, + (unsigned long)delay_millis, + packet ? (uint32_t)packet->getPayloadType() : 0, + packet && packet->isRouteDirect() ? "D" : "F"); + f.close(); + } + } +} + +void MyMesh::onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) { + if (next_hop_hash == NULL || next_hop_hash_len == 0) { + return; + } + + SimpleMeshTables* tables = static_cast(getTables()); + if (tables != NULL) { + if (!tables->decrementRecentRepeaterSnrX4(next_hop_hash, next_hop_hash_len, 1)) { + tables->setRecentRepeater(next_hop_hash, next_hop_hash_len, 7, false, true); + } + } +} + +void MyMesh::onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len, int8_t snr_x4) { + if (next_hop_hash == NULL || next_hop_hash_len == 0) { + return; + } + + SimpleMeshTables* tables = static_cast(getTables()); + if (tables != NULL) { + tables->setRecentRepeater(next_hop_hash, next_hop_hash_len, snr_x4, false, true); + } +} + +static void formatLocalSnrX4(char* dest, size_t dest_len, int16_t snr_x4) { + int16_t v = snr_x4; + const char* sign = ""; + if (v < 0) { + sign = "-"; + v = -v; + } + snprintf(dest, dest_len, "%s%d.%02d", sign, v / 4, (v % 4) * 25); +} + +void MyMesh::formatRecentRepeatersReply(char *reply, int page) { + const SimpleMeshTables* tables = static_cast(getTables()); + if (tables == NULL) { + strcpy(reply, "Error: unsupported"); + return; + } + int count = tables->getRecentRepeaterCount(); + if (count <= 0) { + strcpy(reply, "> -none-"); + return; + } + + const int page_size = 4; + int pages = (count + page_size - 1) / page_size; + if (page < 1) page = 1; + if (page > pages) page = pages; + + int len = snprintf(reply, 160, "> %d/%d ", page, pages); + int start = (page - 1) * page_size; + for (int i = 0; i < page_size && len < 150; i++) { + const SimpleMeshTables::RecentRepeaterInfo* info = tables->getRecentRepeaterNewestByIdx(start + i); + if (info == NULL) break; + char prefix[MAX_ROUTE_HASH_BYTES * 2 + 1]; + char snr[12]; + mesh::Utils::toHex(prefix, info->prefix, info->prefix_len); + prefix[info->prefix_len * 2] = 0; + formatLocalSnrX4(snr, sizeof(snr), info->snr_x4); + len += snprintf(&reply[len], 160 - len, "%s%s,%s", + i == 0 ? "" : " ", + prefix, + snr); + } +} + +bool MyMesh::setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) { + SimpleMeshTables* tables = static_cast(getTables()); + return tables != NULL && tables->setRecentRepeater(prefix, prefix_len, snr_x4, false, true); +} + +void MyMesh::clearRecentRepeaters() { + SimpleMeshTables* tables = static_cast(getTables()); + if (tables != NULL) { + tables->clearRecentRepeaters(); + } +} + bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { // just try to determine region for packet (apply later in allowPacketForward()) if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) { @@ -865,6 +1067,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc next_local_advert = next_flood_advert = 0; dirty_contacts_expiry = 0; set_radio_at = revert_radio_at = 0; + active_bw = 0.0f; + active_sf = 0; + active_cr = 0; _logging = false; region_load_active = false; @@ -894,6 +1099,18 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_max_advert = 8; _prefs.interference_threshold = 0; // disabled _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') + _prefs.retry_preset = RETRY_PRESET_ROOFTOP; + _prefs.direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT; + _prefs.direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS; + _prefs.direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS; + _prefs.direct_retry_snr_margin_x4 = DIRECT_RETRY_ROOFTOP_MARGIN_X4; + _prefs.direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT; + _prefs.direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; + _prefs.direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; + _prefs.direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; + _prefs.direct_retry_cr_enabled = 1; + _prefs.direct_retry_prefs_magic[0] = DIRECT_RETRY_PREFS_MAGIC_0; + _prefs.direct_retry_prefs_magic[1] = DIRECT_RETRY_PREFS_MAGIC_1; // bridge defaults _prefs.bridge_enabled = 1; // enabled @@ -962,6 +1179,9 @@ void MyMesh::begin(FILESYSTEM *fs) { #endif radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + active_bw = _prefs.bw; + active_sf = _prefs.sf; + active_cr = _prefs.cr; radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); @@ -1289,12 +1509,18 @@ void MyMesh::loop() { if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params set_radio_at = 0; // clear timer radio_driver.setParams(pending_freq, pending_bw, pending_sf, pending_cr); + active_bw = pending_bw; + active_sf = pending_sf; + active_cr = pending_cr; MESH_DEBUG_PRINTLN("Temp radio params"); } if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig revert_radio_at = 0; // clear timer radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + active_bw = _prefs.bw; + active_sf = _prefs.sf; + active_cr = _prefs.cr; MESH_DEBUG_PRINTLN("Radio params restored"); } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index bb39c077c4..055b4b8384 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -163,6 +163,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override; uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override; void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override; + void onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) override; + void onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len, int8_t snr_x4) override; int getInterferenceThreshold() const override { return _prefs.interference_threshold; @@ -194,20 +196,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void onControlDataRecv(mesh::Packet* packet) override; void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size); - mesh::Packet* createPacketCopy(const mesh::Packet* packet, const char* caller); - mesh::Packet* createAltPathCopy(const mesh::Packet* packet, - const uint8_t* primary_path, uint8_t primary_path_len, - const uint8_t* alt_path, uint8_t alt_path_len); - void sendFloodReplyWithAltPath(mesh::Packet* packet, - const uint8_t* direct_path, uint8_t direct_path_len, - const uint8_t* alt_path, uint8_t alt_path_len, - unsigned long delay_millis, uint8_t path_hash_size); - void sendDirectWithAltPath(mesh::Packet* packet, - const uint8_t* path, uint8_t path_len, - const uint8_t* alt_path, uint8_t alt_path_len, - uint32_t delay_millis); - void sendFloodScopedWithSelfPath(const TransportKey& scope, mesh::Packet* pkt, - uint32_t delay_millis, uint8_t path_hash_size); public: MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); @@ -248,6 +236,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void formatStatsReply(char *reply) override; void formatRadioStatsReply(char *reply) override; void formatPacketStatsReply(char *reply) override; + void formatRecentRepeatersReply(char *reply, int page) override; + bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) override; + void clearRecentRepeaters() override; void startRegionsLoad() override; bool saveRegions() override; void onDefaultRegionChanged(const RegionEntry* r) override; @@ -257,10 +248,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void saveIdentity(const mesh::LocalIdentity& new_id) override; void clearStats() override; - void handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char* command, char* reply); - void handleCommand(uint32_t sender_timestamp, char* command, char* reply) { - handleCommand(sender_timestamp, NULL, command, reply); - } + void handleCommand(uint32_t sender_timestamp, char* command, char* reply); void loop(); #if defined(WITH_BRIDGE) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 03a89dd46a..2ce056f521 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -53,7 +53,7 @@ void setup() { halt(); } - fast_rng.begin(radio_get_rng_seed()); + fast_rng.begin(radio_driver.getRngSeed()); FILESYSTEM* fs; #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) @@ -99,6 +99,8 @@ void setup() { #if ENABLE_ADVERT_ON_BOOT == 1 the_mesh.sendSelfAdvertisement(16000, false); #endif + + board.onBootComplete(); } void loop() { @@ -120,7 +122,7 @@ void loop() { Serial.print('\n'); command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; - the_mesh.handleCommand(0, NULL, command, reply); // NOTE: there is no sender_timestamp via serial! + the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } diff --git a/src/Mesh.cpp b/src/Mesh.cpp index a9f7aac0c5..0bac461b44 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -35,6 +35,7 @@ void Mesh::begin() { _direct_retries[i].retry_at = 0; _direct_retries[i].retry_delay = 0; _direct_retries[i].retry_attempts_sent = 0; + _direct_retries[i].next_hop_hash_len = 0; _direct_retries[i].priority = 0; _direct_retries[i].progress_marker = 0; _direct_retries[i].expect_path_growth = false; @@ -63,6 +64,7 @@ void Mesh::loop() { : (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at); onDirectRetryEvent("failed_all_tries", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); onDirectRetryEvent("failure", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + onDirectRetryFailed(_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len); clearDirectRetrySlot(i); continue; } @@ -183,12 +185,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } } - if (canDecodeDirectPayloadForSelf(pkt)) { - // Some path sources include the final node hash, and some packets are - // heard before all planned hops are consumed. Only stop forwarding once - // this node proves it can decrypt the payload. - removePathPrefix(pkt, pkt->getPathHashCount()); - } else if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) || maybeShortCircuitDirect(pkt)) { + if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) || maybeShortCircuitDirect(pkt)) { if (allowPacketForward(pkt)) { if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { return forwardMultipartDirect(pkt); @@ -479,13 +476,10 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) { void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { if (!packet->isMarkedDoNotRetransmit()) { - uint32_t crc; - memcpy(&crc, packet->payload, 4); - uint8_t extra = getExtraAckTransmitCount(); while (extra > 0) { delay_millis += getDirectRetransmitDelay(packet) + 300; - auto a1 = createMultiAck(crc, extra); + auto a1 = createMultiAck(packet->payload, packet->payload_len, extra); if (a1) { a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len); a1->header &= ~PH_ROUTE_MASK; @@ -496,7 +490,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { extra--; } - auto a2 = createAck(crc); + auto a2 = createAck(packet->payload, packet->payload_len); if (a2) { a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len); a2->header &= ~PH_ROUTE_MASK; @@ -508,9 +502,6 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { } void Mesh::clearDirectRetrySlot(int idx) { - if (_direct_retries[idx].waiting_final_echo && _direct_retries[idx].packet != NULL) { - releasePacket(_direct_retries[idx].packet); - } _direct_retries[idx].packet = NULL; _direct_retries[idx].trigger_packet = NULL; _direct_retries[idx].retry_started_at = 0; @@ -518,6 +509,8 @@ void Mesh::clearDirectRetrySlot(int idx) { _direct_retries[idx].retry_at = 0; _direct_retries[idx].retry_delay = 0; _direct_retries[idx].retry_attempts_sent = 0; + memset(_direct_retries[idx].next_hop_hash, 0, sizeof(_direct_retries[idx].next_hop_hash)); + _direct_retries[idx].next_hop_hash_len = 0; _direct_retries[idx].priority = 0; _direct_retries[idx].progress_marker = 0; _direct_retries[idx].expect_path_growth = false; @@ -558,6 +551,7 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { } int8_t echo_snr_x4 = packet->_snr; + onDirectRetrySucceeded(_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len, echo_snr_x4); if (_direct_retries[i].queued || _direct_retries[i].waiting_final_echo) { if (_direct_retries[i].packet != NULL) { // Success quality comes from the received downstream echo, not the original upstream RX. @@ -620,16 +614,9 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX; } if (_direct_retries[i].retry_attempts_sent >= max_attempts) { - Packet* final_wait = obtainNewPacket(); - if (final_wait == NULL) { - onDirectRetryEvent("dropped_no_packet", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); - onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); - clearDirectRetrySlot(i); - continue; - } - - *final_wait = *packet; - _direct_retries[i].packet = final_wait; + // Dispatcher releases the retry packet after this hook. Keep only retry metadata + // for the final echo window so pool exhaustion cannot force a premature failure. + _direct_retries[i].packet = NULL; _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); _direct_retries[i].waiting_final_echo = true; _direct_retries[i].queued = false; @@ -783,62 +770,6 @@ bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_h } } -bool Mesh::canDecodeDirectPayloadForSelf(const Packet* packet) { - if (packet == NULL || !packet->isRouteDirect() || packet->getPathHashCount() == 0 || packet->payload_len < 1) { - return false; - } - - switch (packet->getPayloadType()) { - case PAYLOAD_TYPE_PATH: - case PAYLOAD_TYPE_REQ: - case PAYLOAD_TYPE_RESPONSE: - case PAYLOAD_TYPE_TXT_MSG: { - if (packet->payload_len < 2) { - return false; - } - - int i = 0; - uint8_t dest_hash = packet->payload[i++]; - uint8_t src_hash = packet->payload[i++]; - if (i + CIPHER_MAC_SIZE >= packet->payload_len || !self_id.isHashMatch(&dest_hash)) { - return false; - } - - int num = searchPeersByHash(&src_hash); - for (int j = 0; j < num; j++) { - uint8_t secret[PUB_KEY_SIZE]; - getPeerSharedSecret(secret, j); - - uint8_t data[MAX_PACKET_PAYLOAD]; - if (Utils::MACThenDecrypt(secret, data, &packet->payload[i], packet->payload_len - i) > 0) { - return true; - } - } - return false; - } - - case PAYLOAD_TYPE_ANON_REQ: { - int i = 0; - uint8_t dest_hash = packet->payload[i++]; - if (i + PUB_KEY_SIZE + CIPHER_MAC_SIZE >= packet->payload_len || !self_id.isHashMatch(&dest_hash)) { - return false; - } - - Identity sender(&packet->payload[i]); - i += PUB_KEY_SIZE; - - uint8_t secret[PUB_KEY_SIZE]; - self_id.calcSharedSecret(secret, sender); - - uint8_t data[MAX_PACKET_PAYLOAD]; - return Utils::MACThenDecrypt(secret, data, &packet->payload[i], packet->payload_len - i) > 0; - } - - default: - return false; - } -} - void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { const uint8_t* next_hop_hash; uint8_t next_hop_hash_len; @@ -849,6 +780,18 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { return; } + uint8_t retry_key[MAX_HASH_SIZE]; + calculateDirectRetryKey(packet, retry_key); + + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (_direct_retries[i].active + && memcmp(retry_key, _direct_retries[i].retry_key, MAX_HASH_SIZE) == 0 + && _direct_retries[i].progress_marker == progress_marker + && _direct_retries[i].expect_path_growth == expect_path_growth) { + return; + } + } + int slot_idx = -1; for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { if (!_direct_retries[i].active) { @@ -864,7 +807,7 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { // Only store retry metadata here; allocate the retry packet after the initial TX really completes. uint32_t retry_delay = getDirectRetryAttemptDelay(packet, 0); - calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key); + memcpy(_direct_retries[slot_idx].retry_key, retry_key, MAX_HASH_SIZE); _direct_retries[slot_idx].packet = NULL; _direct_retries[slot_idx].trigger_packet = const_cast(packet); _direct_retries[slot_idx].retry_started_at = 0; @@ -872,6 +815,9 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { _direct_retries[slot_idx].retry_at = 0; _direct_retries[slot_idx].retry_delay = retry_delay; _direct_retries[slot_idx].retry_attempts_sent = 0; + memset(_direct_retries[slot_idx].next_hop_hash, 0, sizeof(_direct_retries[slot_idx].next_hop_hash)); + memcpy(_direct_retries[slot_idx].next_hop_hash, next_hop_hash, next_hop_hash_len); + _direct_retries[slot_idx].next_hop_hash_len = next_hop_hash_len; _direct_retries[slot_idx].priority = priority; _direct_retries[slot_idx].progress_marker = progress_marker; _direct_retries[slot_idx].expect_path_growth = expect_path_growth; @@ -1036,7 +982,9 @@ Packet* Mesh::createGroupDatagram(uint8_t type, const GroupChannel& channel, con return packet; } -Packet* Mesh::createAck(uint32_t ack_crc) { +Packet* Mesh::createAck(const uint8_t* ack_hash, uint8_t ack_len) { + if (ack_len > sizeof(Packet::payload)) return NULL; + Packet* packet = obtainNewPacket(); if (packet == NULL) { MESH_DEBUG_PRINTLN("%s Mesh::createAck(): error, packet pool empty", getLogDateTime()); @@ -1044,13 +992,19 @@ Packet* Mesh::createAck(uint32_t ack_crc) { } packet->header = (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later - memcpy(packet->payload, &ack_crc, 4); - packet->payload_len = 4; + memcpy(packet->payload, ack_hash, ack_len); + packet->payload_len = ack_len; return packet; } -Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) { +Packet* Mesh::createAck(uint32_t ack_crc) { + return createAck((const uint8_t*)&ack_crc, 4); +} + +Packet* Mesh::createMultiAck(const uint8_t* ack_hash, uint8_t ack_len, uint8_t remaining) { + if (ack_len + 1 > sizeof(Packet::payload)) return NULL; + Packet* packet = obtainNewPacket(); if (packet == NULL) { MESH_DEBUG_PRINTLN("%s Mesh::createMultiAck(): error, packet pool empty", getLogDateTime()); @@ -1059,12 +1013,16 @@ Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) { packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK; - memcpy(&packet->payload[1], &ack_crc, 4); - packet->payload_len = 5; + memcpy(&packet->payload[1], ack_hash, ack_len); + packet->payload_len = ack_len + 1; return packet; } +Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) { + return createMultiAck((const uint8_t*)&ack_crc, 4, remaining); +} + Packet* Mesh::createRawData(const uint8_t* data, size_t len) { if (len > sizeof(Packet::payload)) return NULL; // invalid arg diff --git a/src/Mesh.h b/src/Mesh.h index 314db23722..d1b66f6272 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -29,6 +29,10 @@ class MeshTables { * and provides virtual methods for sub-classes on handling incoming, and also preparing outbound Packets. */ class Mesh : public Dispatcher { + RNG* _rng; + RTCClock* _rtc; + MeshTables* _tables; + struct DirectRetryEntry { Packet* packet; Packet* trigger_packet; @@ -38,6 +42,8 @@ class Mesh : public Dispatcher { uint32_t retry_delay; uint8_t retry_attempts_sent; uint8_t retry_key[MAX_HASH_SIZE]; + uint8_t next_hop_hash[MAX_HASH_SIZE]; + uint8_t next_hop_hash_len; uint8_t priority; uint8_t progress_marker; bool expect_path_growth; @@ -58,7 +64,6 @@ class Mesh : public Dispatcher { void clearPendingDirectRetryOnSendFail(const Packet* packet); bool getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len, uint8_t& progress_marker, bool& expect_path_growth) const; - bool canDecodeDirectPayloadForSelf(const Packet* packet); void maybeScheduleDirectRetry(const Packet* packet, uint8_t priority); //void routeRecvAcks(Packet* packet, uint32_t delay_millis); DispatcherAction forwardMultipartDirect(Packet* pkt); @@ -133,6 +138,16 @@ class Mesh : public Dispatcher { */ virtual void onDirectRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { } + /** + * \brief Optional hook for link-quality feedback when all direct-retry attempts fail. + */ + virtual void onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) { } + + /** + * \brief Optional hook for link-quality feedback when a direct-retry echo is heard. + */ + virtual void onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len, int8_t snr_x4) { } + /** * \brief Optional hook to set local-only transmit options on a retry packet before it is queued. */ @@ -253,7 +268,9 @@ class Mesh : public Dispatcher { Packet* createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t len); Packet* createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len); Packet* createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len); + Packet* createAck(const uint8_t* ack_hash, uint8_t ack_len); Packet* createAck(uint32_t ack_crc); + Packet* createMultiAck(const uint8_t* ack_hash, uint8_t ack_len, uint8_t remaining); Packet* createMultiAck(uint32_t ack_crc, uint8_t remaining); Packet* createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len); Packet* createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len); diff --git a/src/helpers/ClientACL.cpp b/src/helpers/ClientACL.cpp index 1d88082349..1282382737 100644 --- a/src/helpers/ClientACL.cpp +++ b/src/helpers/ClientACL.cpp @@ -1,7 +1,5 @@ #include "ClientACL.h" -static const uint8_t CONTACT_RECORD_VERSION_ALT_PATH = 1; - static File openWrite(FILESYSTEM* _fs, const char* filename) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) _fs->remove(filename); @@ -30,7 +28,6 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) { uint8_t unused[2]; memset(&c, 0, sizeof(c)); - c.alt_path_len = OUT_PATH_UNKNOWN; bool success = (file.read(pub_key, 32) == 32); success = success && (file.read((uint8_t *) &c.permissions, 1) == 1); @@ -39,10 +36,6 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) { success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1); success = success && (file.read(c.out_path, 64) == 64); success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); // will be recalculated below - if (success && unused[0] >= CONTACT_RECORD_VERSION_ALT_PATH) { - success = success && (file.read((uint8_t *)&c.alt_path_len, 1) == 1); - success = success && (file.read(c.alt_path, 64) == 64); - } if (!success) break; // EOF @@ -64,8 +57,7 @@ void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) { File file = openWrite(_fs, "/s_contacts"); if (file) { uint8_t unused[2]; - unused[0] = CONTACT_RECORD_VERSION_ALT_PATH; - unused[1] = 0; + memset(unused, 0, sizeof(unused)); for (int i = 0; i < num_clients; i++) { auto c = &clients[i]; @@ -78,8 +70,6 @@ void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) { success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1); success = success && (file.write(c->out_path, 64) == 64); success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); - success = success && (file.write((uint8_t *)&c->alt_path_len, 1) == 1); - success = success && (file.write(c->alt_path, 64) == 64); if (!success) break; // write failed } @@ -125,7 +115,6 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) { c->permissions = init_perms; c->id = id; c->out_path_len = OUT_PATH_UNKNOWN; - c->alt_path_len = OUT_PATH_UNKNOWN; return c; } diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index 356574de53..b758f7068d 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -10,16 +10,13 @@ #define PERM_ACL_READ_WRITE 2 #define PERM_ACL_ADMIN 3 -#define OUT_PATH_FORCE_FLOOD 0xFE -#define OUT_PATH_UNKNOWN 0xFF +#define OUT_PATH_UNKNOWN 0xFF struct ClientInfo { mesh::Identity id; uint8_t permissions; uint8_t out_path_len; uint8_t out_path[MAX_PATH_SIZE]; - uint8_t alt_path_len; - uint8_t alt_path[MAX_PATH_SIZE]; uint8_t shared_secret[PUB_KEY_SIZE]; uint32_t last_timestamp; // by THEIR clock (transient) uint32_t last_activity; // by OUR clock (transient) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 82e5374352..687b7f7a29 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -27,6 +27,113 @@ static bool isValidName(const char *n) { return true; } +static bool looksNumeric(const char* s) { + if (s == NULL) return false; + while (*s == ' ') s++; + if (*s == '-' || *s == '+') s++; + bool saw_digit = false; + while (*s) { + if (*s >= '0' && *s <= '9') { + saw_digit = true; + } else if (*s != '.') { + break; + } + s++; + } + return saw_digit; +} + +static int16_t parseSnrDbX4(const char* s) { + float db = atof(s); + return (int16_t)(db * 4.0f + (db >= 0.0f ? 0.5f : -0.5f)); +} + +static void formatSnrDbX4(char* dest, size_t dest_len, int16_t snr_x4) { + int16_t v = snr_x4; + const char* sign = ""; + if (v < 0) { + sign = "-"; + v = -v; + } + snprintf(dest, dest_len, "%s%d.%02d", sign, v / 4, (v % 4) * 25); +} + +static const char* retryPresetName(uint8_t preset) { + switch (preset) { + case RETRY_PRESET_INFRA: return "infra"; + case RETRY_PRESET_ROOFTOP: return "rooftop"; + case RETRY_PRESET_MOBILE: return "mobile"; + default: return "custom"; + } +} + +static void markDirectRetryPrefsValid(NodePrefs* prefs) { + prefs->direct_retry_prefs_magic[0] = DIRECT_RETRY_PREFS_MAGIC_0; + prefs->direct_retry_prefs_magic[1] = DIRECT_RETRY_PREFS_MAGIC_1; +} + +static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) { + prefs->retry_preset = preset; + if (preset == RETRY_PRESET_INFRA) { + prefs->direct_retry_attempts = DIRECT_RETRY_INFRA_COUNT; + prefs->direct_retry_base_ms = DIRECT_RETRY_INFRA_BASE_MS; + prefs->direct_retry_step_ms = DIRECT_RETRY_INFRA_STEP_MS; + prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_INFRA_MARGIN_X4; + } else if (preset == RETRY_PRESET_MOBILE) { + prefs->direct_retry_attempts = DIRECT_RETRY_MOBILE_COUNT; + prefs->direct_retry_base_ms = DIRECT_RETRY_MOBILE_BASE_MS; + prefs->direct_retry_step_ms = DIRECT_RETRY_MOBILE_STEP_MS; + prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_MOBILE_MARGIN_X4; + } else { + prefs->retry_preset = RETRY_PRESET_ROOFTOP; + prefs->direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT; + prefs->direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS; + prefs->direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS; + prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_ROOFTOP_MARGIN_X4; + } + markDirectRetryPrefsValid(prefs); +} + +static void setDefaultDirectRetryPrefs(NodePrefs* prefs) { + applyDirectRetryPreset(prefs, RETRY_PRESET_ROOFTOP); + prefs->direct_retry_cr_enabled = 1; + prefs->direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT; + prefs->direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; + prefs->direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; + prefs->direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; + markDirectRetryPrefsValid(prefs); +} + +static bool directRetryPrefsValid(const NodePrefs* prefs) { + return prefs->direct_retry_prefs_magic[0] == DIRECT_RETRY_PREFS_MAGIC_0 + && prefs->direct_retry_prefs_magic[1] == DIRECT_RETRY_PREFS_MAGIC_1; +} + +static bool parseRetryPreset(const char* s, uint8_t& preset) { + if (strcmp(s, "infra") == 0 || strcmp(s, "0") == 0) { + preset = RETRY_PRESET_INFRA; + return true; + } + if (strcmp(s, "rooftop") == 0 || strcmp(s, "1") == 0) { + preset = RETRY_PRESET_ROOFTOP; + return true; + } + if (strcmp(s, "mobile") == 0 || strcmp(s, "2") == 0) { + preset = RETRY_PRESET_MOBILE; + return true; + } + return false; +} + +static bool parseHashPrefix(const char* text, uint8_t* prefix, uint8_t& prefix_len) { + size_t hex_len = strlen(text); + if (hex_len == 0 || (hex_len & 1) || hex_len > MAX_HASH_SIZE * 2) { + return false; + } + prefix_len = hex_len / 2; + return mesh::Utils::fromHex(prefix, prefix_len, text); +} + void CommonCLI::loadPrefs(FILESYSTEM* fs) { if (fs->exists("/com_prefs")) { loadPrefsInt(fs, "/com_prefs"); // new filename @@ -93,7 +200,18 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 - // next: 295 + file.read((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 295 + file.read((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 296 + file.read((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 297 + file.read((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 299 + file.read((uint8_t *)&_prefs->direct_retry_snr_margin_x4, sizeof(_prefs->direct_retry_snr_margin_x4)); // 301 + file.read((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 303 + file.read((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304 + file.read((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305 + file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306 + file.read((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 307 + file.read((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 308 + // next: 310 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -125,6 +243,17 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean _prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean _prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean + if (!directRetryPrefsValid(_prefs)) { + setDefaultDirectRetryPrefs(_prefs); + } + if (_prefs->retry_preset > RETRY_PRESET_MOBILE && _prefs->retry_preset != RETRY_PRESET_CUSTOM) { + _prefs->retry_preset = RETRY_PRESET_CUSTOM; + } + _prefs->direct_retry_attempts = constrain(_prefs->direct_retry_attempts, 1, 15); + _prefs->direct_retry_base_ms = constrain(_prefs->direct_retry_base_ms, 10, 5000); + _prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, 0, 5000); + _prefs->direct_retry_snr_margin_x4 = constrain(_prefs->direct_retry_snr_margin_x4, 0, 160); + _prefs->direct_retry_cr_enabled = constrain(_prefs->direct_retry_cr_enabled, 0, 1); file.close(); } @@ -190,7 +319,19 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 - // next: 295 + markDirectRetryPrefsValid(_prefs); + file.write((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 295 + file.write((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 296 + file.write((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 297 + file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 299 + file.write((uint8_t *)&_prefs->direct_retry_snr_margin_x4, sizeof(_prefs->direct_retry_snr_margin_x4)); // 301 + file.write((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 303 + file.write((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304 + file.write((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305 + file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306 + file.write((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 307 + file.write((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 308 + // next: 310 file.close(); } @@ -301,6 +442,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else if (memcmp(command, "clear stats", 11) == 0) { _callbacks->clearStats(); strcpy(reply, "(OK - stats reset)"); + } else if (memcmp(command, "clear recent.repeater", 21) == 0 && (command[21] == 0 || command[21] == ' ')) { + _callbacks->clearRecentRepeaters(); + strcpy(reply, "OK"); } else if (memcmp(command, "get ", 4) == 0) { handleGetCmd(sender_timestamp, command, reply); } else if (memcmp(command, "set ", 4) == 0) { @@ -680,6 +824,104 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, must be 0-2"); } + } else if (memcmp(config, "retry.preset ", 13) == 0) { + uint8_t preset; + if (parseRetryPreset(&config[13], preset)) { + applyDirectRetryPreset(_prefs, preset); + savePrefs(); + sprintf(reply, "OK - %s", retryPresetName(_prefs->retry_preset)); + } else { + strcpy(reply, "Error, must be infra, rooftop, or mobile"); + } + } else if (memcmp(config, "direct.retry.margin ", 20) == 0) { + if (!looksNumeric(&config[20])) { + strcpy(reply, "Error, must be 0-40 dB"); + } else { + int16_t margin_x4 = parseSnrDbX4(&config[20]); + if (margin_x4 >= 0 && margin_x4 <= 160) { + _prefs->direct_retry_snr_margin_x4 = (uint16_t)margin_x4; + _prefs->retry_preset = RETRY_PRESET_CUSTOM; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-40 dB"); + } + } + } else if (memcmp(config, "direct.retry.count ", 19) == 0) { + int attempts = _atoi(&config[19]); + if (attempts >= 1 && attempts <= 15) { + _prefs->direct_retry_attempts = (uint8_t)attempts; + _prefs->retry_preset = RETRY_PRESET_CUSTOM; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 1-15"); + } + } else if (memcmp(config, "direct.retry.base ", 18) == 0) { + int base_ms = _atoi(&config[18]); + if (base_ms >= 10 && base_ms <= 5000) { + _prefs->direct_retry_base_ms = (uint16_t)base_ms; + _prefs->retry_preset = RETRY_PRESET_CUSTOM; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 10-5000 ms"); + } + } else if (memcmp(config, "direct.retry.step ", 18) == 0) { + int step_ms = _atoi(&config[18]); + if (step_ms >= 0 && step_ms <= 5000) { + _prefs->direct_retry_step_ms = (uint16_t)step_ms; + _prefs->retry_preset = RETRY_PRESET_CUSTOM; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-5000 ms"); + } + } else if (memcmp(config, "direct.retry.cr ", 16) == 0) { + if (memcmp(&config[16], "off", 3) == 0) { + _prefs->direct_retry_cr_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(tmp, &config[16]); + const char *parts[4]; + int num = mesh::Utils::parseTextParts(tmp, parts, 4, ','); + if (num == 4 && looksNumeric(parts[0]) && looksNumeric(parts[1]) && looksNumeric(parts[2]) && looksNumeric(parts[3])) { + int16_t cr4 = parseSnrDbX4(parts[0]); + int16_t cr5 = parseSnrDbX4(parts[1]); + int16_t cr7 = parseSnrDbX4(parts[2]); + int16_t cr8 = parseSnrDbX4(parts[3]); + if (cr4 >= -128 && cr4 <= 127 && cr5 >= -128 && cr5 <= 127 && cr7 >= -128 && cr7 <= 127 && cr8 >= -128 && cr8 <= 127) { + _prefs->direct_retry_cr4_snr_x4 = (int8_t)cr4; + _prefs->direct_retry_cr5_snr_x4 = (int8_t)cr5; + _prefs->direct_retry_cr7_snr_x4 = (int8_t)cr7; + _prefs->direct_retry_cr8_snr_x4 = (int8_t)cr8; + _prefs->direct_retry_cr_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, SNR must fit -32.00..31.75 dB"); + } + } else { + strcpy(reply, "Error, use CR4,CR5,CR7,CR8 SNRs or off"); + } + } + } else if (memcmp(config, "recent.repeater ", 16) == 0) { + strcpy(tmp, &config[16]); + const char *parts[2]; + int num = mesh::Utils::parseTextParts(tmp, parts, 2, ' '); + uint8_t prefix[MAX_HASH_SIZE]; + uint8_t prefix_len = 0; + int16_t snr_x4 = num > 1 && looksNumeric(parts[1]) ? parseSnrDbX4(parts[1]) : 0; + if (num != 2 || !parseHashPrefix(parts[0], prefix, prefix_len)) { + strcpy(reply, "Error, use: set recent.repeater "); + } else if (snr_x4 < -128 || snr_x4 > 127) { + strcpy(reply, "Error, SNR must fit -32.00..31.75 dB"); + } else if (_callbacks->setRecentRepeater(prefix, prefix_len, (int8_t)snr_x4)) { + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, table rejected prefix"); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; @@ -862,6 +1104,37 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); } else if (memcmp(config, "direct.txdelay", 14) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); + } else if (memcmp(config, "retry.preset", 12) == 0) { + sprintf(reply, "> %s", retryPresetName(_prefs->retry_preset)); + } else if (memcmp(config, "direct.retry.margin", 19) == 0) { + char margin[12]; + formatSnrDbX4(margin, sizeof(margin), _prefs->direct_retry_snr_margin_x4); + sprintf(reply, "> %s", margin); + } else if (memcmp(config, "direct.retry.count", 18) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts); + } else if (memcmp(config, "direct.retry.base", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_base_ms); + } else if (memcmp(config, "direct.retry.step", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_step_ms); + } else if (memcmp(config, "direct.retry.cr", 15) == 0) { + if (!_prefs->direct_retry_cr_enabled) { + strcpy(reply, "> off"); + } else { + char cr4[12], cr5[12], cr7[12], cr8[12]; + formatSnrDbX4(cr4, sizeof(cr4), _prefs->direct_retry_cr4_snr_x4); + formatSnrDbX4(cr5, sizeof(cr5), _prefs->direct_retry_cr5_snr_x4); + formatSnrDbX4(cr7, sizeof(cr7), _prefs->direct_retry_cr7_snr_x4); + formatSnrDbX4(cr8, sizeof(cr8), _prefs->direct_retry_cr8_snr_x4); + sprintf(reply, "> %s,%s,%s,%s", cr4, cr5, cr7, cr8); + } + } else if (memcmp(config, "recent.repeater", 15) == 0) { + int page = 1; + const char* cursor = &config[15]; + while (*cursor == ' ') cursor++; + if (memcmp(cursor, "page ", 5) == 0) cursor += 5; + if (*cursor) page = _atoi(cursor); + if (page < 1) page = 1; + _callbacks->formatRecentRepeatersReply(reply, page); } else if (memcmp(config, "owner.info", 10) == 0) { auto start = reply; *reply++ = '>'; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 10cb00c776..7fa71405a4 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -19,6 +19,34 @@ #define LOOP_DETECT_MODERATE 2 #define LOOP_DETECT_STRICT 3 +#define RETRY_PRESET_INFRA 0 +#define RETRY_PRESET_ROOFTOP 1 +#define RETRY_PRESET_MOBILE 2 +#define RETRY_PRESET_CUSTOM 0xFF + +#define DIRECT_RETRY_INFRA_BASE_MS 275 +#define DIRECT_RETRY_INFRA_COUNT 4 +#define DIRECT_RETRY_INFRA_STEP_MS 150 +#define DIRECT_RETRY_INFRA_MARGIN_X4 60 + +#define DIRECT_RETRY_ROOFTOP_BASE_MS 175 +#define DIRECT_RETRY_ROOFTOP_COUNT 15 +#define DIRECT_RETRY_ROOFTOP_STEP_MS 100 +#define DIRECT_RETRY_ROOFTOP_MARGIN_X4 20 + +#define DIRECT_RETRY_MOBILE_BASE_MS 175 +#define DIRECT_RETRY_MOBILE_COUNT 15 +#define DIRECT_RETRY_MOBILE_STEP_MS 50 +#define DIRECT_RETRY_MOBILE_MARGIN_X4 0 + +#define DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT 40 +#define DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT 30 +#define DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT 10 +#define DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT 10 + +#define DIRECT_RETRY_PREFS_MAGIC_0 0xD1 +#define DIRECT_RETRY_PREFS_MAGIC_1 0x52 + struct NodePrefs { // persisted to file float airtime_factor; char node_name[32]; @@ -65,6 +93,17 @@ struct NodePrefs { // persisted to file uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; uint8_t cad_enabled; // hardware Channel Activity Detection before TX (boolean) + uint8_t retry_preset; + uint8_t direct_retry_attempts; + uint16_t direct_retry_base_ms; + uint16_t direct_retry_step_ms; + uint16_t direct_retry_snr_margin_x4; + int8_t direct_retry_cr4_snr_x4; + int8_t direct_retry_cr5_snr_x4; + int8_t direct_retry_cr7_snr_x4; + int8_t direct_retry_cr8_snr_x4; + uint8_t direct_retry_cr_enabled; + uint8_t direct_retry_prefs_magic[2]; }; class CommonCLICallbacks { @@ -88,6 +127,18 @@ class CommonCLICallbacks { virtual void formatStatsReply(char *reply) = 0; virtual void formatRadioStatsReply(char *reply) = 0; virtual void formatPacketStatsReply(char *reply) = 0; + virtual void formatRecentRepeatersReply(char *reply, int page) { + (void)page; + if (reply != NULL) reply[0] = 0; + } + virtual bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) { + (void)prefix; + (void)prefix_len; + (void)snr_x4; + return false; + } + virtual void clearRecentRepeaters() { + } virtual mesh::LocalIdentity& getSelfId() = 0; virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0; virtual void clearStats() = 0; diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 2d2125dd1c..f1d5273361 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -9,8 +9,7 @@ #include #endif -#define MAX_PACKET_HASHES 128 -#define MAX_PACKET_ACKS 64 +#define MAX_PACKET_HASHES (128+32) #ifndef MAX_RECENT_REPEATERS // Platform defaults. Can be overridden with -D MAX_RECENT_REPEATERS=. #if defined(ESP32) || defined(ESP32_PLATFORM) @@ -39,8 +38,6 @@ class SimpleMeshTables : public mesh::MeshTables { private: uint8_t _hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE]; int _next_idx; - uint32_t _acks[MAX_PACKET_ACKS]; - int _next_ack_idx; uint32_t _direct_dups, _flood_dups; RecentRepeaterInfo _recent_repeaters[MAX_RECENT_REPEATERS]; int _next_recent_repeater_idx; @@ -48,20 +45,6 @@ class SimpleMeshTables : public mesh::MeshTables { RecentRepeaterAllowFn _recent_repeater_allow_fn; void* _recent_repeater_allow_ctx; - bool hasSeenAck(uint32_t ack) const { - for (int i = 0; i < MAX_PACKET_ACKS; i++) { - if (ack == _acks[i]) { - return true; - } - } - return false; - } - - void storeAck(uint32_t ack) { - _acks[_next_ack_idx] = ack; - _next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS; - } - bool hasSeenHash(const uint8_t* hash) const { const uint8_t* sp = _hashes; for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { @@ -148,8 +131,6 @@ class SimpleMeshTables : public mesh::MeshTables { SimpleMeshTables() { memset(_hashes, 0, sizeof(_hashes)); _next_idx = 0; - memset(_acks, 0, sizeof(_acks)); - _next_ack_idx = 0; _direct_dups = _flood_dups = 0; memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); _next_recent_repeater_idx = 0; @@ -162,8 +143,6 @@ class SimpleMeshTables : public mesh::MeshTables { void restoreFrom(File f) { f.read(_hashes, sizeof(_hashes)); f.read((uint8_t *) &_next_idx, sizeof(_next_idx)); - f.read((uint8_t *) &_acks[0], sizeof(_acks)); - f.read((uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx)); // Recent repeater entries are intentionally not restored across boots. // This avoids struct-layout migration issues and keeps stale path quality // stats from persisting indefinitely. @@ -173,31 +152,10 @@ class SimpleMeshTables : public mesh::MeshTables { void saveTo(File f) { f.write(_hashes, sizeof(_hashes)); f.write((const uint8_t *) &_next_idx, sizeof(_next_idx)); - f.write((const uint8_t *) &_acks[0], sizeof(_acks)); - f.write((const uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx)); } #endif bool hasSeen(const mesh::Packet* packet) override { - if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { - recordRecentRepeater(packet); - - uint32_t ack; - memcpy(&ack, packet->payload, 4); - - if (hasSeenAck(ack)) { - if (packet->isRouteDirect()) { - _direct_dups++; // keep some stats - } else { - _flood_dups++; - } - return true; - } - - storeAck(ack); - return false; - } - uint8_t hash[MAX_HASH_SIZE]; packet->calculatePacketHash(hash); @@ -217,15 +175,6 @@ class SimpleMeshTables : public mesh::MeshTables { void markSent(const mesh::Packet* packet) override { // Outbound packets must be marked as already-sent without teaching the recent-heard cache about ourselves. - if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { - uint32_t ack; - memcpy(&ack, packet->payload, 4); - if (!hasSeenAck(ack)) { - storeAck(ack); - } - return; - } - uint8_t hash[MAX_HASH_SIZE]; packet->calculatePacketHash(hash); if (!hasSeenHash(hash)) { @@ -234,25 +183,14 @@ class SimpleMeshTables : public mesh::MeshTables { } void clear(const mesh::Packet* packet) override { - if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { - uint32_t ack; - memcpy(&ack, packet->payload, 4); - for (int i = 0; i < MAX_PACKET_ACKS; i++) { - if (ack == _acks[i]) { - _acks[i] = 0; - break; - } - } - } else { - uint8_t hash[MAX_HASH_SIZE]; - packet->calculatePacketHash(hash); - - uint8_t* sp = _hashes; - for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { - if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { - memset(sp, 0, MAX_HASH_SIZE); - break; - } + uint8_t hash[MAX_HASH_SIZE]; + packet->calculatePacketHash(hash); + + uint8_t* sp = _hashes; + for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { + if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { + memset(sp, 0, MAX_HASH_SIZE); + break; } } } @@ -282,17 +220,13 @@ class SimpleMeshTables : public mesh::MeshTables { return false; } - // Keep one slot for overlapping prefixes so 1/2/3-byte paths share the same entry. + // Keep exact prefixes distinct so a 1-byte path prefix does not collapse + // independent 2/3-byte repeaters that share the same first byte. for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { RecentRepeaterInfo& existing = _recent_repeaters[i]; - if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + if (existing.prefix_len != prefix_len || memcmp(existing.prefix, prefix, prefix_len) != 0) { continue; } - if (prefix_len > existing.prefix_len) { - memset(existing.prefix, 0, sizeof(existing.prefix)); - memcpy(existing.prefix, prefix, prefix_len); - existing.prefix_len = prefix_len; - } if (snr_locked) { existing.snr_x4 = snr_x4; existing.snr_locked = 1; @@ -352,14 +286,9 @@ class SimpleMeshTables : public mesh::MeshTables { for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { RecentRepeaterInfo& existing = _recent_repeaters[i]; - if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + if (existing.prefix_len != prefix_len || memcmp(existing.prefix, prefix, prefix_len) != 0) { continue; } - if (prefix_len > existing.prefix_len) { - memset(existing.prefix, 0, sizeof(existing.prefix)); - memcpy(existing.prefix, prefix, prefix_len); - existing.prefix_len = prefix_len; - } if (!existing.snr_locked) { int16_t lowered = (int16_t)existing.snr_x4 - (int16_t)amount_x4; if (lowered < -128) { @@ -432,18 +361,25 @@ class SimpleMeshTables : public mesh::MeshTables { return NULL; } - // Search newest-to-oldest and allow 1/2/3-byte prefixes to overlap-match. + // Prefer exact matches. If none exists, fall back to the newest longest + // overlapping prefix so coarse learned prefixes can still inform CR. + const RecentRepeaterInfo* best = NULL; for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; const RecentRepeaterInfo* info = &_recent_repeaters[idx]; if (info->prefix_len == 0) { continue; } - if (prefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) { + if (info->prefix_len == hash_len && memcmp(info->prefix, hash, hash_len) == 0) { return info; } + if (prefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) { + if (best == NULL || info->prefix_len > best->prefix_len) { + best = info; + } + } } - return NULL; + return best; } void clearRecentRepeaters() { memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); From cb5e2e7c3d49ef921a63ce446918958ed46ff12d Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 17:42:53 -0700 Subject: [PATCH 85/94] Document direct retry CLI --- docs/cli_commands.md | 224 ++++++++++++++++++++++++++++ examples/simple_repeater/MyMesh.cpp | 7 +- src/helpers/CommonCLI.cpp | 30 +++- src/helpers/CommonCLI.h | 1 + 4 files changed, 255 insertions(+), 7 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index c06f5e12b3..2d31a18bad 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -950,6 +950,230 @@ region save - Enables flooding for all child regions automatically - Useful for global networks with specific regional rules +--- +### Direct Retry + +Direct retry resends direct-routed packets when the downstream echo is not heard. It applies to direct messages and TRACE packets. It does not change ACK handling. + +#### View or change direct retry state +**Usage:** +- `get direct.retry` +- `set direct.retry ` + +**Parameters:** +- `state`: `on`|`off` + +**Default:** `on` + +**Examples:** +``` +get direct.retry +set direct.retry on +set direct.retry off +``` + +--- + +#### View or apply a direct retry preset +**Usage:** +- `get retry.preset` +- `set retry.preset ` + +**Parameters:** +- `preset`: `infra`|`rooftop`|`mobile` + +**Notes:** +- `infra`: fewer, slower retries for stable fixed infrastructure. +- `rooftop`: default long retry window for weak rooftop links. +- `mobile`: long retry count with shorter spacing for moving or changing links. +- Changing `direct.retry.count`, `direct.retry.base`, `direct.retry.step`, or `direct.retry.margin` makes the preset report as `custom`. + +**Examples:** +``` +get retry.preset +set retry.preset infra +set retry.preset rooftop +set retry.preset mobile +``` + +--- + +#### View or change direct retry count +**Usage:** +- `get direct.retry.count` +- `set direct.retry.count ` + +**Parameters:** +- `count`: Maximum retry attempts after the original send, from `1` to `15`. + +**Default:** `15` with the `rooftop` preset + +**Examples:** +``` +get direct.retry.count +set direct.retry.count 1 +set direct.retry.count 4 +set direct.retry.count 15 +``` + +--- + +#### View or change direct retry base delay +**Usage:** +- `get direct.retry.base` +- `set direct.retry.base ` + +**Parameters:** +- `ms`: First retry wait in milliseconds, from `10` to `5000`. + +**Default:** `175` with the `rooftop` preset + +**Explanation:** +- The first retry waits `base` milliseconds after the failed echo window. +- Larger values reduce channel pressure and give slow repeaters more time. +- Smaller values recover faster but create tighter retry bursts. + +**Examples:** +``` +get direct.retry.base +set direct.retry.base 175 +set direct.retry.base 275 +set direct.retry.base 500 +``` + +--- + +#### View or change direct retry step delay +**Usage:** +- `get direct.retry.step` +- `set direct.retry.step ` + +**Parameters:** +- `ms`: Extra milliseconds added for each subsequent retry, from `0` to `5000`. + +**Default:** `100` with the `rooftop` preset + +**Explanation:** +- Retry delay is `base + attempt_index * step`. +- With `base=175` and `step=100`, retries wait about `175`, `275`, `375`, `475` ms, and so on. +- `step=0` keeps every retry at the same delay. +- Larger steps spread retries over time and are safer on busy channels. + +**Examples:** +``` +get direct.retry.step +set direct.retry.step 0 +set direct.retry.step 50 +set direct.retry.step 100 +set direct.retry.step 250 +``` + +--- + +#### View or change direct retry SNR margin +**Usage:** +- `get direct.retry.margin` +- `set direct.retry.margin ` + +**Parameters:** +- `snr_db`: Extra SNR margin above the SF receive floor, from `0` to `40`. + +**Default:** `5.00` with the `rooftop` preset + +**Notes:** +- Unknown repeaters are still retried. +- Known repeaters below the receive floor plus this margin are skipped. +- Failed attempts lower the recent repeater SNR estimate by `0.25 dB`. + +**Examples:** +``` +get direct.retry.margin +set direct.retry.margin 0 +set direct.retry.margin 2.5 +set direct.retry.margin 5 +set direct.retry.margin 10 +``` + +--- + +#### View or change adaptive direct retry coding rate +**Usage:** +- `get direct.retry.cr` +- `set direct.retry.cr off` +- `set direct.retry.cr ,,,` + +**Parameters:** +- `cr4_min`: Minimum SNR in dB to retry at CR4. +- `cr5_min`: Minimum SNR in dB to retry at CR5. +- `cr7_min`: Minimum SNR in dB to retry at CR7. +- `cr8_max`: Maximum SNR in dB that forces CR8. + +**Default:** `10.00,7.50,2.50,2.50` + +**Explanation:** +- Higher SNR uses faster coding rates. +- Lower SNR uses more robust coding rates. +- CR6 is intentionally skipped. +- `off` disables per-packet retry CR overrides and uses the current radio CR. +- Unknown repeaters start at `+2.00 dB` for adaptive CR selection. +- A failed unknown repeater is seeded at `+1.75 dB`. +- Each later failure lowers the SNR estimate by `0.25 dB`. + +**Examples:** +``` +get direct.retry.cr +set direct.retry.cr off +set direct.retry.cr 10.0,7.5,2.5,2.5 +set direct.retry.cr 12.0,8.0,4.0,1.0 +set direct.retry.cr 8.0,5.0,1.5,0 +set direct.retry.cr 6.0,3.0,0,-2.0 +set direct.retry.cr 20.0,12.0,6.0,2.0 +set direct.retry.cr 4.0,2.0,0,-4.0 +``` + +**Example profiles:** +- Conservative weak-link profile: +``` +set direct.retry.cr 12.0,8.0,4.0,1.0 +``` +- Balanced rooftop profile: +``` +set direct.retry.cr 10.0,7.5,2.5,2.5 +``` +- Faster strong-link profile: +``` +set direct.retry.cr 6.0,3.0,0,-2.0 +``` +- Very cautious noisy-link profile: +``` +set direct.retry.cr 20.0,12.0,6.0,2.0 +``` + +--- + +#### View, seed, or clear the recent repeater table +**Usage:** +- `get recent.repeater` +- `get recent.repeater ` +- `get recent.repeater page ` +- `set recent.repeater ` +- `clear recent.repeater` + +**Parameters:** +- `prefix`: Repeater path-hash prefix as hex. +- `snr_db`: SNR in dB. +- `page`: 1-based result page. + +**Examples:** +``` +get recent.repeater +get recent.repeater 2 +get recent.repeater page 3 +set recent.repeater A1B2C3 8.5 +set recent.repeater 71CE82 -3.25 +clear recent.repeater +``` + --- ### GPS (When GPS support is compiled in) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 45f81ed241..f8946e64a9 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -575,7 +575,8 @@ uint8_t MyMesh::getDirectRetryCodingRateForSNR(int8_t snr_x4) const { if (snr_x4 >= _prefs.direct_retry_cr4_snr_x4) return 4; if (snr_x4 >= _prefs.direct_retry_cr5_snr_x4) return 5; if (snr_x4 >= _prefs.direct_retry_cr7_snr_x4) return 7; - return 8; + if (snr_x4 <= _prefs.direct_retry_cr8_snr_x4) return 8; + return 7; } uint8_t MyMesh::getDirectRetryPreset() const { @@ -592,6 +593,9 @@ uint32_t MyMesh::getDirectRetryAttemptStepMillis() const { bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { (void)packet; + if (!_prefs.direct_retry_enabled) { + return false; + } if (next_hop_hash == NULL || next_hop_hash_len == 0) { return true; } @@ -1108,6 +1112,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; _prefs.direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; _prefs.direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; + _prefs.direct_retry_enabled = 1; _prefs.direct_retry_cr_enabled = 1; _prefs.direct_retry_prefs_magic[0] = DIRECT_RETRY_PREFS_MAGIC_0; _prefs.direct_retry_prefs_magic[1] = DIRECT_RETRY_PREFS_MAGIC_1; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 687b7f7a29..4dfbbf6d21 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -101,6 +101,7 @@ static void setDefaultDirectRetryPrefs(NodePrefs* prefs) { prefs->direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; prefs->direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; prefs->direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; + prefs->direct_retry_enabled = 1; markDirectRetryPrefsValid(prefs); } @@ -209,9 +210,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304 file.read((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305 file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306 - file.read((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 307 - file.read((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 308 - // next: 310 + file.read((uint8_t *)&_prefs->direct_retry_enabled, sizeof(_prefs->direct_retry_enabled)); // 307 + file.read((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 308 + file.read((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 309 + // next: 311 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -253,6 +255,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->direct_retry_base_ms = constrain(_prefs->direct_retry_base_ms, 10, 5000); _prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, 0, 5000); _prefs->direct_retry_snr_margin_x4 = constrain(_prefs->direct_retry_snr_margin_x4, 0, 160); + _prefs->direct_retry_enabled = constrain(_prefs->direct_retry_enabled, 0, 1); _prefs->direct_retry_cr_enabled = constrain(_prefs->direct_retry_cr_enabled, 0, 1); file.close(); @@ -329,9 +332,10 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304 file.write((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305 file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306 - file.write((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 307 - file.write((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 308 - // next: 310 + file.write((uint8_t *)&_prefs->direct_retry_enabled, sizeof(_prefs->direct_retry_enabled)); // 307 + file.write((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 308 + file.write((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 309 + // next: 311 file.close(); } @@ -833,6 +837,18 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, must be infra, rooftop, or mobile"); } + } else if (memcmp(config, "direct.retry ", 13) == 0) { + if (memcmp(&config[13], "on", 2) == 0) { + _prefs->direct_retry_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(&config[13], "off", 3) == 0) { + _prefs->direct_retry_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be on or off"); + } } else if (memcmp(config, "direct.retry.margin ", 20) == 0) { if (!looksNumeric(&config[20])) { strcpy(reply, "Error, must be 0-40 dB"); @@ -1106,6 +1122,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); } else if (memcmp(config, "retry.preset", 12) == 0) { sprintf(reply, "> %s", retryPresetName(_prefs->retry_preset)); + } else if (memcmp(config, "direct.retry", 12) == 0 && (config[12] == 0 || config[12] == ' ')) { + sprintf(reply, "> %s", _prefs->direct_retry_enabled ? "on" : "off"); } else if (memcmp(config, "direct.retry.margin", 19) == 0) { char margin[12]; formatSnrDbX4(margin, sizeof(margin), _prefs->direct_retry_snr_margin_x4); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 7fa71405a4..76a7cf4623 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -102,6 +102,7 @@ struct NodePrefs { // persisted to file int8_t direct_retry_cr5_snr_x4; int8_t direct_retry_cr7_snr_x4; int8_t direct_retry_cr8_snr_x4; + uint8_t direct_retry_enabled; uint8_t direct_retry_cr_enabled; uint8_t direct_retry_prefs_magic[2]; }; From 21c5e2701bf771f0fd5de638a57edf3104dbbd1c Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 18:06:58 -0700 Subject: [PATCH 86/94] Tune unknown retry SNR --- docs/cli_commands.md | 13 +++++++++++-- examples/simple_repeater/MyMesh.cpp | 6 +++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 2d31a18bad..6adcc4c791 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1115,8 +1115,8 @@ set direct.retry.margin 10 - Lower SNR uses more robust coding rates. - CR6 is intentionally skipped. - `off` disables per-packet retry CR overrides and uses the current radio CR. -- Unknown repeaters start at `+2.00 dB` for adaptive CR selection. -- A failed unknown repeater is seeded at `+1.75 dB`. +- Unknown repeaters start at `+3.00 dB` for adaptive CR selection. +- A failed unknown repeater is seeded at `+2.75 dB`. - Each later failure lowers the SNR estimate by `0.25 dB`. **Examples:** @@ -1164,6 +1164,15 @@ set direct.retry.cr 20.0,12.0,6.0,2.0 - `snr_db`: SNR in dB. - `page`: 1-based result page. +**SNR details:** +- Recent repeater SNR is stored internally in quarter-dB units. +- Heard repeater samples update an existing table entry with a weighted blend: `75%` existing SNR and `25%` new heard SNR, rounded up. +- Direct retry success also feeds the heard echo SNR back into the same weighted table. +- Direct retry failure is not weighted: each final echo-timeout failure lowers that repeater's SNR by `0.25 dB`. +- Unknown repeaters start at `+3.00 dB` for adaptive CR selection. +- If an unknown repeater fails, it is seeded into the table at `+2.75 dB`. +- `set recent.repeater ` seeds a missing prefix or adds another weighted sample for an existing prefix. + **Examples:** ``` get recent.repeater diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index f8946e64a9..05c308bc13 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -606,7 +606,7 @@ bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_ho if (repeater == NULL) { // Retry unknown repeaters too. If they fail, onDirectRetryFailed() seeds the - // recent-repeater table below the +2.00 dB starting point. + // recent-repeater table below the +3.00 dB starting point. return true; } int16_t retry_floor_x4 = (int16_t)getDirectRetryMinSNRX4() + (int16_t)_prefs.direct_retry_snr_margin_x4; @@ -615,7 +615,7 @@ bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_ho void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) { (void)retry_attempt; - int8_t snr_x4 = 8; // unknown repeaters start at +2.00 dB + int8_t snr_x4 = 12; // unknown repeaters start at +3.00 dB const SimpleMeshTables* tables = static_cast(getTables()); if (tables != NULL) { uint8_t prefix[MAX_HASH_SIZE]; @@ -683,7 +683,7 @@ void MyMesh::onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_ SimpleMeshTables* tables = static_cast(getTables()); if (tables != NULL) { if (!tables->decrementRecentRepeaterSnrX4(next_hop_hash, next_hop_hash_len, 1)) { - tables->setRecentRepeater(next_hop_hash, next_hop_hash_len, 7, false, true); + tables->setRecentRepeater(next_hop_hash, next_hop_hash_len, 11, false, true); } } } From e35886780c9d3ce6a11c38d561e2e94abb6361ac Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 18:10:40 -0700 Subject: [PATCH 87/94] Simplify recent repeater paging --- docs/cli_commands.md | 2 -- src/helpers/CommonCLI.cpp | 1 - 2 files changed, 3 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 6adcc4c791..50a14b77aa 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1155,7 +1155,6 @@ set direct.retry.cr 20.0,12.0,6.0,2.0 **Usage:** - `get recent.repeater` - `get recent.repeater ` -- `get recent.repeater page ` - `set recent.repeater ` - `clear recent.repeater` @@ -1177,7 +1176,6 @@ set direct.retry.cr 20.0,12.0,6.0,2.0 ``` get recent.repeater get recent.repeater 2 -get recent.repeater page 3 set recent.repeater A1B2C3 8.5 set recent.repeater 71CE82 -3.25 clear recent.repeater diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 4dfbbf6d21..78cd45b911 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -1149,7 +1149,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep int page = 1; const char* cursor = &config[15]; while (*cursor == ' ') cursor++; - if (memcmp(cursor, "page ", 5) == 0) cursor += 5; if (*cursor) page = _atoi(cursor); if (page < 1) page = 1; _callbacks->formatRecentRepeatersReply(reply, page); From 4ed9415866d635cb6cbfcbb9d14076276b4bc245 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 18:15:00 -0700 Subject: [PATCH 88/94] Clean up Mesh comments --- src/Mesh.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Mesh.h b/src/Mesh.h index d1b66f6272..4511b39466 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -190,7 +190,6 @@ class Mesh : public Dispatcher { /** * \brief A path TO peer (sender_idx) has been received. (also with optional 'extra' data encoded) - * NOTE: these can be received multiple times (per sender), via differen routes * \param sender_idx index of peer, [0..n) where n is what searchPeersByHash() returned * \param secret the pre-calculated shared-secret (handy for sending response packet) * \returns true, if path was accepted and that reciprocal path should be sent @@ -213,7 +212,6 @@ class Mesh : public Dispatcher { /** * \brief A path TO 'sender' has been received. (also with optional 'extra' data encoded) - * NOTE: these can be received multiple times (per sender), via differen routes */ virtual void onPathRecv(Packet* packet, Identity& sender, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { } @@ -295,12 +293,10 @@ class Mesh : public Dispatcher { void sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis=0); /** - * \brief send a locally-generated Packet to just neigbor nodes (zero hops) */ void sendZeroHop(Packet* packet, uint32_t delay_millis=0); /** - * \brief send a locally-generated Packet to just neigbor nodes (zero hops), with specific transort codes * \param transport_codes array of 2 codes to attach to packet */ void sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0); From 7d2ff381207c3b84ee083ebc0fe45bfb653c5151 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 16 Jun 2026 13:15:52 -0700 Subject: [PATCH 89/94] Trim direct retry cleanup --- examples/simple_repeater/MyMesh.cpp | 13 ------------- examples/simple_repeater/MyMesh.h | 3 --- src/Mesh.cpp | 5 ++--- src/Mesh.h | 10 ++++++++-- 4 files changed, 10 insertions(+), 21 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 05c308bc13..4261edd995 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -579,10 +579,6 @@ uint8_t MyMesh::getDirectRetryCodingRateForSNR(int8_t snr_x4) const { return 7; } -uint8_t MyMesh::getDirectRetryPreset() const { - return _prefs.retry_preset; -} - uint8_t MyMesh::getDirectRetryConfiguredMaxAttempts() const { return constrain(_prefs.direct_retry_attempts, 1, 15); } @@ -631,11 +627,6 @@ void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* retry->tx_cr = getDirectRetryCodingRateForSNR(snr_x4); } -bool MyMesh::maybeShortCircuitDirect(mesh::Packet* packet) { - (void)packet; - return false; -} - uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { (void)packet; return 200; @@ -1071,7 +1062,6 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc next_local_advert = next_flood_advert = 0; dirty_contacts_expiry = 0; set_radio_at = revert_radio_at = 0; - active_bw = 0.0f; active_sf = 0; active_cr = 0; _logging = false; @@ -1184,7 +1174,6 @@ void MyMesh::begin(FILESYSTEM *fs) { #endif radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); - active_bw = _prefs.bw; active_sf = _prefs.sf; active_cr = _prefs.cr; radio_driver.setTxPower(_prefs.tx_power_dbm); @@ -1514,7 +1503,6 @@ void MyMesh::loop() { if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params set_radio_at = 0; // clear timer radio_driver.setParams(pending_freq, pending_bw, pending_sf, pending_cr); - active_bw = pending_bw; active_sf = pending_sf; active_cr = pending_cr; MESH_DEBUG_PRINTLN("Temp radio params"); @@ -1523,7 +1511,6 @@ void MyMesh::loop() { if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig revert_radio_at = 0; // clear timer radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); - active_bw = _prefs.bw; active_sf = _prefs.sf; active_cr = _prefs.cr; MESH_DEBUG_PRINTLN("Radio params restored"); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 055b4b8384..fddbed3e51 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -110,7 +110,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { unsigned long set_radio_at, revert_radio_at; float pending_freq; float pending_bw; - float active_bw; // live BW, including temporary radio overrides uint8_t pending_sf; uint8_t active_sf; // live SF, including temporary radio overrides uint8_t pending_cr; @@ -125,7 +124,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { bool extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const; int8_t getDirectRetryMinSNRX4() const; uint8_t getDirectRetryCodingRateForSNR(int8_t snr_x4) const; - uint8_t getDirectRetryPreset() const; uint8_t getDirectRetryConfiguredMaxAttempts() const; uint32_t getDirectRetryAttemptStepMillis() const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); @@ -157,7 +155,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; uint8_t getDefaultTxCodingRate() const override { return active_cr; } bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override; - bool maybeShortCircuitDirect(mesh::Packet* packet) override; void configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) override; uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override; uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 0bac461b44..a4b2750dfa 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -185,7 +185,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } } - if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) || maybeShortCircuitDirect(pkt)) { + if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize())) { if (allowPacketForward(pkt)) { if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { return forwardMultipartDirect(pkt); @@ -448,9 +448,8 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) { packet->setPathHashCount(n + 1); uint32_t d = getRetransmitDelay(packet); - uint8_t priority = packet->getPathHashCount(); // as this propagates outwards, give it lower and lower priority - return ACTION_RETRANSMIT_DELAYED(priority, d); // give priority to closer sources, than ones further away + return ACTION_RETRANSMIT_DELAYED(packet->getPathHashCount(), d); // give priority to closer sources, than ones further away } return ACTION_RELEASE; } diff --git a/src/Mesh.h b/src/Mesh.h index 4511b39466..523c204b7e 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -111,8 +111,6 @@ class Mesh : public Dispatcher { /** * \brief Allow subclasses to rewrite a non-TRACE DIRECT packet path when this node can safely skip ahead. */ - virtual bool maybeShortCircuitDirect(Packet* packet) { return false; } - /** * \returns milliseconds to wait for the next-hop echo before queueing one retry of the DIRECT packet. */ @@ -190,6 +188,8 @@ class Mesh : public Dispatcher { /** * \brief A path TO peer (sender_idx) has been received. (also with optional 'extra' data encoded) + * NOTE: these can be received multiple times (per sender), via different routes + * NOTE: these can be received multiple times (per sender), via differen routes * \param sender_idx index of peer, [0..n) where n is what searchPeersByHash() returned * \param secret the pre-calculated shared-secret (handy for sending response packet) * \returns true, if path was accepted and that reciprocal path should be sent @@ -212,6 +212,8 @@ class Mesh : public Dispatcher { /** * \brief A path TO 'sender' has been received. (also with optional 'extra' data encoded) + * NOTE: these can be received multiple times (per sender), via different routes + * NOTE: these can be received multiple times (per sender), via differen routes */ virtual void onPathRecv(Packet* packet, Identity& sender, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { } @@ -293,10 +295,14 @@ class Mesh : public Dispatcher { void sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis=0); /** + * \brief send a locally-generated Packet to just neighbor nodes (zero hops) + * \brief send a locally-generated Packet to just neigbor nodes (zero hops) */ void sendZeroHop(Packet* packet, uint32_t delay_millis=0); /** + * \brief send a locally-generated Packet to just neighbor nodes (zero hops), with specific transport codes + * \brief send a locally-generated Packet to just neigbor nodes (zero hops), with specific transort codes * \param transport_codes array of 2 codes to attach to packet */ void sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0); From f58cdef8a82d50c704af970f7d5553c83022e7f4 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 16 Jun 2026 13:21:51 -0700 Subject: [PATCH 90/94] Revert Mesh comment noise --- src/Mesh.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mesh.h b/src/Mesh.h index 523c204b7e..81842aa1e1 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -82,7 +82,7 @@ class Mesh : public Dispatcher { /** * \brief Called _before_ the packet is dispatched to the on..Recv() methods. - * \returns true, if given packet should be NOT be processed. + * \returns true, if given packet should NOT be processed. */ virtual bool filterRecvFloodPacket(Packet* packet) { return false; } From 4a9d6bae52f228ad7c4af90ba356c19ce5447c09 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 16 Jun 2026 13:25:04 -0700 Subject: [PATCH 91/94] Remove duplicate Mesh comments --- src/Mesh.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Mesh.h b/src/Mesh.h index 81842aa1e1..04c8b84be4 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -189,7 +189,6 @@ class Mesh : public Dispatcher { /** * \brief A path TO peer (sender_idx) has been received. (also with optional 'extra' data encoded) * NOTE: these can be received multiple times (per sender), via different routes - * NOTE: these can be received multiple times (per sender), via differen routes * \param sender_idx index of peer, [0..n) where n is what searchPeersByHash() returned * \param secret the pre-calculated shared-secret (handy for sending response packet) * \returns true, if path was accepted and that reciprocal path should be sent @@ -213,7 +212,6 @@ class Mesh : public Dispatcher { /** * \brief A path TO 'sender' has been received. (also with optional 'extra' data encoded) * NOTE: these can be received multiple times (per sender), via different routes - * NOTE: these can be received multiple times (per sender), via differen routes */ virtual void onPathRecv(Packet* packet, Identity& sender, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { } @@ -296,13 +294,11 @@ class Mesh : public Dispatcher { /** * \brief send a locally-generated Packet to just neighbor nodes (zero hops) - * \brief send a locally-generated Packet to just neigbor nodes (zero hops) */ void sendZeroHop(Packet* packet, uint32_t delay_millis=0); /** * \brief send a locally-generated Packet to just neighbor nodes (zero hops), with specific transport codes - * \brief send a locally-generated Packet to just neigbor nodes (zero hops), with specific transort codes * \param transport_codes array of 2 codes to attach to packet */ void sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0); From 1f14f744af90244e6160f764d490bc1f6df28810 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 16 Jun 2026 13:28:46 -0700 Subject: [PATCH 92/94] Clean Mesh header comments --- src/Mesh.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Mesh.h b/src/Mesh.h index 04c8b84be4..b342aed2c4 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -29,8 +29,8 @@ class MeshTables { * and provides virtual methods for sub-classes on handling incoming, and also preparing outbound Packets. */ class Mesh : public Dispatcher { - RNG* _rng; RTCClock* _rtc; + RNG* _rng; MeshTables* _tables; struct DirectRetryEntry { @@ -104,13 +104,10 @@ class Mesh : public Dispatcher { /** * \brief Decide whether a DIRECT packet should get one delayed retry if the next hop echo is not overheard. - * Sub-classes can use neighbour tables or other link-quality data to opt in selectively. + * Sub-classes can use recent repeater or other link-quality data to opt in selectively. */ virtual bool allowDirectRetry(const Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const; - /** - * \brief Allow subclasses to rewrite a non-TRACE DIRECT packet path when this node can safely skip ahead. - */ /** * \returns milliseconds to wait for the next-hop echo before queueing one retry of the DIRECT packet. */ From 9c510ab7a65db218fca16b101c60914d628f42fd Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 16 Jun 2026 13:30:49 -0700 Subject: [PATCH 93/94] Add Packet EOF newline --- src/Packet.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Packet.cpp b/src/Packet.cpp index 3aab6349c5..a542a5a27e 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -87,3 +87,4 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) { } } + From 8d46ed4f21b6b7abc113bb102024635057d00fe1 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 16 Jun 2026 16:25:04 -0700 Subject: [PATCH 94/94] Enable retry CR radio overrides --- docs/cli_commands.md | 8 +++--- src/Dispatcher.cpp | 23 +++++++++++++--- src/Dispatcher.h | 6 ++++- src/helpers/CommonCLI.cpp | 28 ++++++++++++++++---- src/helpers/radiolib/CustomLLCC68Wrapper.h | 4 +++ src/helpers/radiolib/CustomLR1110Wrapper.h | 4 +++ src/helpers/radiolib/CustomSTM32WLxWrapper.h | 4 +++ src/helpers/radiolib/CustomSX1262Wrapper.h | 4 +++ src/helpers/radiolib/CustomSX1268Wrapper.h | 4 +++ src/helpers/radiolib/CustomSX1276Wrapper.h | 4 +++ src/helpers/radiolib/RadioLibWrappers.h | 2 ++ 11 files changed, 78 insertions(+), 13 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 50a14b77aa..9c816cf0f6 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1155,12 +1155,12 @@ set direct.retry.cr 20.0,12.0,6.0,2.0 **Usage:** - `get recent.repeater` - `get recent.repeater ` -- `set recent.repeater ` +- `set recent.repeater [snr_db]` - `clear recent.repeater` **Parameters:** - `prefix`: Repeater path-hash prefix as hex. -- `snr_db`: SNR in dB. +- `snr_db`: Optional SNR in dB. If omitted or invalid, defaults to `3.0`. - `page`: 1-based result page. **SNR details:** @@ -1170,7 +1170,8 @@ set direct.retry.cr 20.0,12.0,6.0,2.0 - Direct retry failure is not weighted: each final echo-timeout failure lowers that repeater's SNR by `0.25 dB`. - Unknown repeaters start at `+3.00 dB` for adaptive CR selection. - If an unknown repeater fails, it is seeded into the table at `+2.75 dB`. -- `set recent.repeater ` seeds a missing prefix or adds another weighted sample for an existing prefix. +- `set recent.repeater [snr_db]` seeds a missing prefix or adds another weighted sample for an existing prefix. +- Successful `set recent.repeater` replies include the stored prefix and SNR, for example `OK - set A1B2C3 at 3.0 SNR`. **Examples:** ``` @@ -1178,6 +1179,7 @@ get recent.repeater get recent.repeater 2 set recent.repeater A1B2C3 8.5 set recent.repeater 71CE82 -3.25 +set recent.repeater A1B2C3 clear recent.repeater ``` diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index b870be9e16..63112f85d8 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -52,11 +52,15 @@ void Dispatcher::updateTxBudget() { } } -void Dispatcher::restoreOutboundCodingRate() { +void Dispatcher::restoreOutboundTxOverrides() { if (outbound_restore_cr != 0) { _radio->setCodingRate(outbound_restore_cr); outbound_restore_cr = 0; } + if (outbound_restore_preamble_len != 0) { + _radio->setPreambleLength(outbound_restore_preamble_len); + outbound_restore_preamble_len = 0; + } } int Dispatcher::calcRxDelay(float score, uint32_t air_time) const { @@ -113,7 +117,7 @@ void Dispatcher::loop() { } _radio->onSendFinished(); - restoreOutboundCodingRate(); + restoreOutboundTxOverrides(); logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); onSendComplete(outbound); if (outbound->isRouteFlood()) { @@ -127,7 +131,7 @@ void Dispatcher::loop() { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime()); _radio->onSendFinished(); - restoreOutboundCodingRate(); + restoreOutboundTxOverrides(); logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); onSendFail(outbound); @@ -342,6 +346,7 @@ void Dispatcher::checkSend() { uint32_t max_airtime = _radio->getEstAirtimeFor(len)*3/2; outbound_restore_cr = 0; + outbound_restore_preamble_len = 0; uint8_t default_cr = getDefaultTxCodingRate(); if (outbound->tx_cr >= 4 && outbound->tx_cr <= 8 && default_cr >= 4 && default_cr <= 8 && outbound->tx_cr != default_cr) { @@ -352,12 +357,22 @@ void Dispatcher::checkSend() { MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): WARN: failed to set packet CR%d", getLogDateTime(), (uint32_t)outbound->tx_cr); } } + bool has_direct_path = outbound->getPathHashCount() > 0 + || (outbound->getPayloadType() == PAYLOAD_TYPE_TRACE && outbound->payload_len > 9); + if (outbound->isRouteDirect() && has_direct_path + && (outbound->tx_cr == 4 || outbound->tx_cr == 5)) { + uint16_t default_preamble_len = _radio->getDefaultPreambleLength(); + if (default_preamble_len > 16 && _radio->setPreambleLength(16)) { + outbound_restore_preamble_len = default_preamble_len; + max_airtime = _radio->getEstAirtimeFor(len)*3/2; + } + } outbound_start = _ms->getMillis(); bool success = _radio->startSendRaw(raw, len); if (!success) { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): ERROR: send start failed!", getLogDateTime()); - restoreOutboundCodingRate(); + restoreOutboundTxOverrides(); logTxFail(outbound, outbound->getRawLength()); onSendFail(outbound); diff --git a/src/Dispatcher.h b/src/Dispatcher.h index e6daae4beb..cb86bb4b7e 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -51,6 +51,8 @@ class Radio { * \returns true if the radio accepted the coding rate. */ virtual bool setCodingRate(uint8_t cr) { return false; } + virtual uint16_t getDefaultPreambleLength() const { return 0; } + virtual bool setPreambleLength(uint16_t len) { return false; } /** * \returns true if the previous 'startSendRaw()' completed successfully. @@ -124,6 +126,7 @@ typedef uint32_t DispatcherAction; class Dispatcher { Packet* outbound; // current outbound packet unsigned long outbound_expiry, outbound_start, total_air_time, rx_air_time; + uint16_t outbound_restore_preamble_len; uint8_t outbound_restore_cr; unsigned long next_tx_time; unsigned long cad_busy_start; @@ -137,7 +140,7 @@ class Dispatcher { unsigned long duty_cycle_window_ms; void processRecvPacket(Packet* pkt); - void restoreOutboundCodingRate(); + void restoreOutboundTxOverrides(); void updateTxBudget(); protected: @@ -150,6 +153,7 @@ class Dispatcher { : _radio(&radio), _ms(&ms), _mgr(&mgr) { outbound = NULL; + outbound_restore_preamble_len = 0; outbound_restore_cr = 0; total_air_time = rx_air_time = 0; next_tx_time = ms.getMillis(); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 78cd45b911..4c17922353 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -9,6 +9,8 @@ #define BRIDGE_MAX_BAUD 115200 #endif +#define RECENT_REPEATER_PREFIX_MAX_BYTES 3 + // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -128,13 +130,21 @@ static bool parseRetryPreset(const char* s, uint8_t& preset) { static bool parseHashPrefix(const char* text, uint8_t* prefix, uint8_t& prefix_len) { size_t hex_len = strlen(text); - if (hex_len == 0 || (hex_len & 1) || hex_len > MAX_HASH_SIZE * 2) { + if (hex_len == 0 || (hex_len & 1) || hex_len > RECENT_REPEATER_PREFIX_MAX_BYTES * 2) { return false; } prefix_len = hex_len / 2; return mesh::Utils::fromHex(prefix, prefix_len, text); } +static void formatSnrDbX4Short(char* dest, size_t dest_len, int16_t snr_x4) { + formatSnrDbX4(dest, dest_len, snr_x4); + size_t len = strlen(dest); + if (len > 3 && dest[len - 1] == '0') { + dest[len - 1] = 0; + } +} + void CommonCLI::loadPrefs(FILESYSTEM* fs) { if (fs->exists("/com_prefs")) { loadPrefsInt(fs, "/com_prefs"); // new filename @@ -928,13 +938,21 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep int num = mesh::Utils::parseTextParts(tmp, parts, 2, ' '); uint8_t prefix[MAX_HASH_SIZE]; uint8_t prefix_len = 0; - int16_t snr_x4 = num > 1 && looksNumeric(parts[1]) ? parseSnrDbX4(parts[1]) : 0; - if (num != 2 || !parseHashPrefix(parts[0], prefix, prefix_len)) { - strcpy(reply, "Error, use: set recent.repeater "); + int16_t snr_x4 = 12; // default to +3.0 dB when omitted or invalid + if (num > 1 && looksNumeric(parts[1])) { + snr_x4 = parseSnrDbX4(parts[1]); + } + if (num < 1 || !parseHashPrefix(parts[0], prefix, prefix_len)) { + strcpy(reply, "Error, use: set recent.repeater [snr_db]"); } else if (snr_x4 < -128 || snr_x4 > 127) { strcpy(reply, "Error, SNR must fit -32.00..31.75 dB"); } else if (_callbacks->setRecentRepeater(prefix, prefix_len, (int8_t)snr_x4)) { - strcpy(reply, "OK"); + char prefix_hex[RECENT_REPEATER_PREFIX_MAX_BYTES * 2 + 1]; + char snr[12]; + mesh::Utils::toHex(prefix_hex, prefix, prefix_len); + prefix_hex[prefix_len * 2] = 0; + formatSnrDbX4Short(snr, sizeof(snr), snr_x4); + sprintf(reply, "OK - set %s at %s SNR", prefix_hex, snr); } else { strcpy(reply, "Error, table rejected prefix"); } diff --git a/src/helpers/radiolib/CustomLLCC68Wrapper.h b/src/helpers/radiolib/CustomLLCC68Wrapper.h index 8861f76d24..620ef8640b 100644 --- a/src/helpers/radiolib/CustomLLCC68Wrapper.h +++ b/src/helpers/radiolib/CustomLLCC68Wrapper.h @@ -16,6 +16,10 @@ class CustomLLCC68Wrapper : public RadioLibWrapper { updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomLLCC68 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + bool isReceivingPacket() override { return ((CustomLLCC68 *)_radio)->isReceiving(); } diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 13efd25b57..4d30f515ed 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -16,6 +16,10 @@ class CustomLR1110Wrapper : public RadioLibWrapper { updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomLR1110 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); } bool isReceivingPacket() override { return ((CustomLR1110 *)_radio)->isReceiving(); diff --git a/src/helpers/radiolib/CustomSTM32WLxWrapper.h b/src/helpers/radiolib/CustomSTM32WLxWrapper.h index 97bf6820d6..45e275a3fd 100644 --- a/src/helpers/radiolib/CustomSTM32WLxWrapper.h +++ b/src/helpers/radiolib/CustomSTM32WLxWrapper.h @@ -17,6 +17,10 @@ class CustomSTM32WLxWrapper : public RadioLibWrapper { updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomSTM32WLx *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + bool isReceivingPacket() override { return ((CustomSTM32WLx *)_radio)->isReceiving(); } diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index cc7bb2238b..66f7adbfba 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -20,6 +20,10 @@ class CustomSX1262Wrapper : public RadioLibWrapper { updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomSX1262 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + bool isReceivingPacket() override { return ((CustomSX1262 *)_radio)->isReceiving(); } diff --git a/src/helpers/radiolib/CustomSX1268Wrapper.h b/src/helpers/radiolib/CustomSX1268Wrapper.h index 9ddea78f3f..b87e1cd645 100644 --- a/src/helpers/radiolib/CustomSX1268Wrapper.h +++ b/src/helpers/radiolib/CustomSX1268Wrapper.h @@ -20,6 +20,10 @@ class CustomSX1268Wrapper : public RadioLibWrapper { updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomSX1268 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + bool isReceivingPacket() override { return ((CustomSX1268 *)_radio)->isReceiving(); } diff --git a/src/helpers/radiolib/CustomSX1276Wrapper.h b/src/helpers/radiolib/CustomSX1276Wrapper.h index 9d75ce12a1..cb073f91bc 100644 --- a/src/helpers/radiolib/CustomSX1276Wrapper.h +++ b/src/helpers/radiolib/CustomSX1276Wrapper.h @@ -19,6 +19,10 @@ class CustomSX1276Wrapper : public RadioLibWrapper { updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomSX1276 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + bool isReceivingPacket() override { return ((CustomSX1276 *)_radio)->isReceiving(); } diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 9943bcab77..0774023421 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -40,6 +40,8 @@ class RadioLibWrapper : public mesh::Radio { } virtual void setParams(float freq, float bw, uint8_t sf, uint8_t cr) = 0; + uint16_t getDefaultPreambleLength() const override { return preambleLengthForSF(_preamble_sf); } + bool setPreambleLength(uint16_t len) override { return _radio->setPreambleLength(len) == RADIOLIB_ERR_NONE; } uint32_t getRngSeed(); void setTxPower(int8_t dbm);