Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0243ec41e8 | ||
|
|
756268e2ee | ||
|
|
9c2ac5aa1c | ||
|
|
577433ce47 | ||
|
|
6a53f6de27 | ||
|
|
fae6dd83fd | ||
|
|
8525b4e980 | ||
|
|
d8552e3344 | ||
|
|
4c393348df | ||
|
|
166f804da1 | ||
|
|
0ce1a55017 | ||
|
|
00c14cd88c |
No files matched your search
@@ -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 - <<EOF
|
||||
Usage:
|
||||
sh build.sh <command> [target]
|
||||
bash build.sh <command> [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 <build-match-spec>
|
||||
$ bash build.sh build-matching-firmwares <build-match-spec>
|
||||
|
||||
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,61 +76,427 @@ 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:"
|
||||
get_pio_envs() {
|
||||
pio project config | grep 'env:' | sed 's/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
|
||||
}
|
||||
|
||||
# Catch cries for help before doing anything else.
|
||||
case $1 in
|
||||
help|usage|-h|--help)
|
||||
global_usage
|
||||
exit 1
|
||||
;;
|
||||
list|-l)
|
||||
get_pio_envs
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
get_pio_envs() {
|
||||
if [ ${#ALL_PIO_ENVS[@]} -gt 0 ]; then
|
||||
printf '%s\n' "${ALL_PIO_ENVS[@]}"
|
||||
else
|
||||
pio project config | grep 'env:' | sed 's/env://'
|
||||
fi
|
||||
}
|
||||
|
||||
# cache project config json for use in get_platform_for_env()
|
||||
PIO_CONFIG_JSON=$(pio project config --json-output)
|
||||
canonicalize_variant_suffix() {
|
||||
local variant_suffix=$1
|
||||
|
||||
# $1 should be the string to find (case insensitive)
|
||||
get_pio_envs_containing_string() {
|
||||
shopt -s nocasematch
|
||||
envs=($(get_pio_envs))
|
||||
for env in "${envs[@]}"; do
|
||||
if [[ "$env" == *${1}* ]]; then
|
||||
echo $env
|
||||
fi
|
||||
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
|
||||
}
|
||||
|
||||
# $1 should be the string to find (case insensitive)
|
||||
get_pio_envs_ending_with_string() {
|
||||
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
|
||||
envs=($(get_pio_envs))
|
||||
for env in "${envs[@]}"; do
|
||||
if [[ "$env" == *${1} ]]; then
|
||||
echo $env
|
||||
# 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
|
||||
}
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
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 platform flag for a given environment
|
||||
# $1 should be the environment name
|
||||
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}"
|
||||
}
|
||||
|
||||
get_pio_envs_containing_string() {
|
||||
local env
|
||||
|
||||
shopt -s nocasematch
|
||||
for env in "${ALL_PIO_ENVS[@]}"; do
|
||||
if [[ "$env" == *${1}* ]]; then
|
||||
echo "$env"
|
||||
fi
|
||||
done
|
||||
shopt -u nocasematch
|
||||
}
|
||||
|
||||
get_pio_envs_ending_with_string() {
|
||||
local env
|
||||
|
||||
shopt -s nocasematch
|
||||
for env in "${ALL_PIO_ENVS[@]}"; do
|
||||
if [[ "$env" == *${1} ]]; then
|
||||
echo "$env"
|
||||
fi
|
||||
done
|
||||
shopt -u nocasematch
|
||||
}
|
||||
|
||||
get_platform_for_env() {
|
||||
local env_name=$1
|
||||
echo "$PIO_CONFIG_JSON" | python3 -c "
|
||||
|
||||
# PlatformIO exposes project config as JSON; scan the selected env's
|
||||
# build_flags to recover the platform token used for artifact collection.
|
||||
# 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:
|
||||
@@ -101,142 +504,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)
|
||||
"
|
||||
" <<<"$PIO_CONFIG_JSON"
|
||||
}
|
||||
|
||||
is_supported_platform() {
|
||||
local env_platform=$1
|
||||
|
||||
[[ "$env_platform" =~ ^(${SUPPORTED_PLATFORM_PATTERN})$ ]]
|
||||
}
|
||||
|
||||
# disable all debug logging flags if DISABLE_DEBUG=1 is set
|
||||
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
|
||||
copy_build_output() {
|
||||
local source_path=$1
|
||||
local output_path=$2
|
||||
|
||||
if [ -f "$source_path" ]; then
|
||||
cp -- "$source_path" "$output_path"
|
||||
fi
|
||||
}
|
||||
|
||||
collect_esp32_artifacts() {
|
||||
local env_name=$1
|
||||
local firmware_filename=$2
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
collect_nrf52_artifacts() {
|
||||
local env_name=$1
|
||||
local firmware_filename=$2
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
collect_stm32_artifacts() {
|
||||
local env_name=$1
|
||||
local firmware_filename=$2
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
collect_rp2040_artifacts() {
|
||||
local env_name=$1
|
||||
local firmware_filename=$2
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
build_firmware() {
|
||||
# get env platform for post build actions
|
||||
ENV_PLATFORM=($(get_platform_for_env $1))
|
||||
local env_name=$1
|
||||
local env_platform
|
||||
local commit_hash
|
||||
local firmware_build_date
|
||||
local firmware_version_string
|
||||
local firmware_filename
|
||||
|
||||
# get git commit sha
|
||||
COMMIT_HASH=$(git rev-parse --short HEAD)
|
||||
|
||||
# set firmware build date
|
||||
FIRMWARE_BUILD_DATE=$(date '+%d-%b-%Y')
|
||||
|
||||
# get FIRMWARE_VERSION, which should be provided by the environment
|
||||
if [ -z "$FIRMWARE_VERSION" ]; then
|
||||
echo "FIRMWARE_VERSION must be set in environment"
|
||||
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
|
||||
|
||||
# set firmware version string
|
||||
# e.g: v1.0.0-abcdef
|
||||
FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}-${COMMIT_HASH}"
|
||||
commit_hash=$(git rev-parse --short HEAD)
|
||||
firmware_build_date=$(date '+%d-%b-%Y')
|
||||
|
||||
# craft filename
|
||||
# e.g: RAK_4631_Repeater-v1.0.0-SHA
|
||||
FIRMWARE_FILENAME="$1-${FIRMWARE_VERSION_STRING}"
|
||||
if [ -z "$FIRMWARE_VERSION" ]; then
|
||||
prompt_for_firmware_version "$env_name"
|
||||
echo "Using firmware version: ${FIRMWARE_VERSION}"
|
||||
fi
|
||||
|
||||
# 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}\"'"
|
||||
firmware_version_string="${FIRMWARE_VERSION}-${commit_hash}"
|
||||
firmware_filename="${env_name}-${firmware_version_string}"
|
||||
|
||||
# disable debug flags if requested
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DFIRMWARE_BUILD_DATE='\"${firmware_build_date}\"' -DFIRMWARE_VERSION='\"${firmware_version_string}\"'"
|
||||
disable_debug_flags
|
||||
|
||||
# build firmware target
|
||||
pio run -e $1
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
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
|
||||
fi
|
||||
|
||||
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 +660,87 @@ 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
|
||||
if [ -z "$output_dir" ] || [ "$output_dir" == "/" ] || [ "$output_dir" == "." ]; then
|
||||
echo "Refusing to clean unsafe output directory: $output_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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 <target>"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ $1 == "build-matching-firmwares" ]]; then
|
||||
if [ "$2" ]; then
|
||||
build_all_firmwares_matching $2
|
||||
else
|
||||
echo "usage: $0 build-matching-firmwares <build-match-spec>"
|
||||
exit 1
|
||||
|
||||
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 <build-match-spec>"
|
||||
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
|
||||
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
|
||||
|
||||
prepare_output_dir
|
||||
run_command "$@"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -115,6 +115,30 @@ 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 <count> <offset>`
|
||||
- `get recent.repeater first <count>`
|
||||
- `get recent.repeater first <count> <offset>`
|
||||
- `get recent.repeater last <count>`
|
||||
- `get recent.repeater last <count> <offset>`
|
||||
- `set recent.repeater <prefix_hex> <snr_db>`
|
||||
|
||||
**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.
|
||||
- Over LoRa remote CLI, replies are packet-size limited; use `offset` to page through all rows.
|
||||
|
||||
---
|
||||
|
||||
## Statistics
|
||||
|
||||
### Clear Stats
|
||||
@@ -504,6 +528,60 @@ 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 <state>`
|
||||
|
||||
**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 <value>`
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
#### View or change the number of direct retry attempts
|
||||
**Usage:**
|
||||
- `get direct.retry.count`
|
||||
- `set direct.retry.count <value>`
|
||||
|
||||
**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 <value>`
|
||||
|
||||
**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`
|
||||
|
||||
@@ -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,29 @@
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
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 +425,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 +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);
|
||||
@@ -544,6 +600,49 @@ 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 {
|
||||
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 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 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) {
|
||||
// just try to determine region for packet (apply later in allowPacketForward())
|
||||
@@ -874,6 +973,10 @@ 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_base_ms = 200;
|
||||
StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
|
||||
_prefs.node_lat = ADVERT_LAT;
|
||||
_prefs.node_lon = ADVERT_LON;
|
||||
@@ -914,8 +1017,11 @@ 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;
|
||||
|
||||
memset(default_scope.key, 0, sizeof(default_scope.key));
|
||||
((SimpleMeshTables *)getTables())->setRecentRepeaterAllowFilter(&MyMesh::allowRecentRepeaterPrefixStore, this);
|
||||
}
|
||||
|
||||
void MyMesh::begin(FILESYSTEM *fs) {
|
||||
@@ -954,6 +1060,10 @@ 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;
|
||||
((SimpleMeshTables *)getTables())->setRecentRepeaterMinSNRX4(getDirectRetryMinSNRX4());
|
||||
radio_set_tx_power(_prefs.tx_power_dbm);
|
||||
|
||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||
@@ -1242,6 +1352,196 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
||||
Serial.printf("\n");
|
||||
}
|
||||
reply[0] = 0;
|
||||
} 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();
|
||||
|
||||
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) {
|
||||
strcpy(reply, "Err - usage: set recent.repeater <prefix_hex> <snr_db>");
|
||||
} else {
|
||||
*arg_snr++ = 0;
|
||||
while (*arg_snr == ' ') arg_snr++;
|
||||
if (*arg_snr == 0) {
|
||||
strcpy(reply, "Err - usage: set recent.repeater <prefix_hex> <snr_db>");
|
||||
} 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 (*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) {
|
||||
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 <count> [offset]");
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 <count> <offset>");
|
||||
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 <count> <offset>");
|
||||
return;
|
||||
}
|
||||
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);
|
||||
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);
|
||||
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", idx + 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... use offset");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
strcpy(reply, "Err - usage: get recent.repeater [all|all <count> <offset>|first <count> [offset]|last <count> [offset]]");
|
||||
}
|
||||
} else if (memcmp(command, "discover.neighbors", 18) == 0) {
|
||||
const char* sub = command + 18;
|
||||
while (*sub == ' ') sub++;
|
||||
@@ -1280,15 +1580,24 @@ 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");
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -110,8 +110,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;
|
||||
@@ -119,6 +122,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
ESPNowBridge bridge;
|
||||
#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);
|
||||
uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
|
||||
@@ -146,6 +152,10 @@ protected:
|
||||
|
||||
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;
|
||||
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 {
|
||||
return _prefs.interference_threshold;
|
||||
|
||||
+3
-1
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -159,6 +159,8 @@ protected:
|
||||
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;
|
||||
@@ -168,6 +170,7 @@ protected:
|
||||
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();
|
||||
|
||||
+331
-6
@@ -3,12 +3,40 @@
|
||||
|
||||
namespace mesh {
|
||||
|
||||
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++) {
|
||||
_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;
|
||||
_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)) {
|
||||
if (_direct_retries[i].packet == getOutboundInFlight()) {
|
||||
continue; // currently transmitting; keep slot until onSendComplete/onSendFail emits event
|
||||
}
|
||||
clearDirectRetrySlot(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Mesh::allowPacketForward(const mesh::Packet* packet) {
|
||||
@@ -22,10 +50,33 @@ 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::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;
|
||||
}
|
||||
|
||||
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 +90,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 +113,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 +154,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 +429,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 +440,277 @@ 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].retry_attempts_sent = 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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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.
|
||||
onDirectRetryEvent("resent", packet, 0);
|
||||
_direct_retries[i].retry_attempts_sent++;
|
||||
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;
|
||||
}
|
||||
|
||||
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 = getDirectRetryAttemptDelay(packet, _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;
|
||||
}
|
||||
|
||||
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) {
|
||||
onDirectRetryEvent("dropped_no_packet", packet, _direct_retries[i].retry_delay);
|
||||
onDirectRetryEvent("failure", packet, 0);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
// 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;
|
||||
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() == 0) {
|
||||
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 = 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*>(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) {
|
||||
if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL;
|
||||
|
||||
@@ -634,7 +958,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 +987,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 +1020,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 +1031,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 +1044,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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+57
@@ -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 @@ public:
|
||||
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,43 @@ public:
|
||||
* 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_attempts_sent;
|
||||
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,11 +96,37 @@ protected:
|
||||
*/
|
||||
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 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.
|
||||
*/
|
||||
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
|
||||
|
||||
+505
-6
@@ -8,6 +8,21 @@
|
||||
#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
|
||||
#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) {
|
||||
uint32_t n = 0;
|
||||
@@ -60,7 +75,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
|
||||
@@ -88,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);
|
||||
@@ -103,6 +123,23 @@ 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);
|
||||
}
|
||||
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);
|
||||
@@ -151,7 +188,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
|
||||
@@ -179,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();
|
||||
}
|
||||
@@ -290,9 +335,463 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
_callbacks->clearStats();
|
||||
strcpy(reply, "(OK - stats reset)");
|
||||
} else if (memcmp(command, "get ", 4) == 0) {
|
||||
handleGetCmd(sender_timestamp, command, reply);
|
||||
const char* config = &command[4];
|
||||
if (memcmp(config, "af", 2) == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor));
|
||||
} else if (memcmp(config, "int.thresh", 10) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold);
|
||||
} else if (memcmp(config, "agc.reset.interval", 18) == 0) {
|
||||
sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
|
||||
} else if (memcmp(config, "multi.acks", 10) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks);
|
||||
} else if (memcmp(config, "allow.read.only", 15) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off");
|
||||
} else if (memcmp(config, "flood.advert.interval", 21) == 0) {
|
||||
sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval));
|
||||
} else if (memcmp(config, "advert.interval", 15) == 0) {
|
||||
sprintf(reply, "> %d", ((uint32_t) _prefs->advert_interval) * 2);
|
||||
} else if (memcmp(config, "guest.password", 14) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->guest_password);
|
||||
} else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) { // from serial command line only
|
||||
uint8_t prv_key[PRV_KEY_SIZE];
|
||||
int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE);
|
||||
mesh::Utils::toHex(tmp, prv_key, len);
|
||||
sprintf(reply, "> %s", tmp);
|
||||
} else if (memcmp(config, "name", 4) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->node_name);
|
||||
} else if (memcmp(config, "repeat", 6) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->disable_fwd ? "off" : "on");
|
||||
} else if (memcmp(config, "lat", 3) == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat));
|
||||
} else if (memcmp(config, "lon", 3) == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon));
|
||||
#if defined(USE_SX1262) || defined(USE_SX1268)
|
||||
} else if (memcmp(config, "radio.rxgain", 12) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off");
|
||||
#endif
|
||||
} else if (memcmp(config, "radio", 5) == 0) {
|
||||
char freq[16], bw[16];
|
||||
strcpy(freq, StrHelper::ftoa(_prefs->freq));
|
||||
strcpy(bw, StrHelper::ftoa3(_prefs->bw));
|
||||
sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr);
|
||||
} else if (memcmp(config, "rxdelay", 7) == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base));
|
||||
} else if (memcmp(config, "txdelay", 7) == 0) {
|
||||
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, "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, "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++ = ' ';
|
||||
const char* sp = _prefs->owner_info;
|
||||
while (*sp) {
|
||||
*reply++ = (*sp == '\n') ? '|' : *sp; // translate newline back to orig '|'
|
||||
sp++;
|
||||
}
|
||||
*reply = 0; // set null terminator
|
||||
} else if (memcmp(config, "path.hash.mode", 14) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode);
|
||||
} else if (memcmp(config, "loop.detect", 11) == 0) {
|
||||
if (_prefs->loop_detect == LOOP_DETECT_OFF) {
|
||||
strcpy(reply, "> off");
|
||||
} else if (_prefs->loop_detect == LOOP_DETECT_MINIMAL) {
|
||||
strcpy(reply, "> minimal");
|
||||
} else if (_prefs->loop_detect == LOOP_DETECT_MODERATE) {
|
||||
strcpy(reply, "> moderate");
|
||||
} 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) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq));
|
||||
} else if (memcmp(config, "public.key", 10) == 0) {
|
||||
strcpy(reply, "> ");
|
||||
mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE);
|
||||
} else if (memcmp(config, "role", 4) == 0) {
|
||||
sprintf(reply, "> %s", _callbacks->getRole());
|
||||
} else if (memcmp(config, "bridge.type", 11) == 0) {
|
||||
sprintf(reply, "> %s",
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
"rs232"
|
||||
#elif WITH_ESPNOW_BRIDGE
|
||||
"espnow"
|
||||
#else
|
||||
"none"
|
||||
#endif
|
||||
);
|
||||
#ifdef WITH_BRIDGE
|
||||
} else if (memcmp(config, "bridge.enabled", 14) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->bridge_enabled ? "on" : "off");
|
||||
} else if (memcmp(config, "bridge.delay", 12) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_delay);
|
||||
} else if (memcmp(config, "bridge.source", 13) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->bridge_pkt_src ? "logRx" : "logTx");
|
||||
#endif
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
} else if (memcmp(config, "bridge.baud", 11) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud);
|
||||
#endif
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
} else if (memcmp(config, "bridge.channel", 14) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel);
|
||||
} else if (memcmp(config, "bridge.secret", 13) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->bridge_secret);
|
||||
#endif
|
||||
} else if (memcmp(config, "bootloader.ver", 14) == 0) {
|
||||
#ifdef NRF52_PLATFORM
|
||||
char ver[32];
|
||||
if (_board->getBootloaderVersion(ver, sizeof(ver))) {
|
||||
sprintf(reply, "> %s", ver);
|
||||
} else {
|
||||
strcpy(reply, "> unknown");
|
||||
}
|
||||
#else
|
||||
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");
|
||||
} else {
|
||||
sprintf(reply, "> %.3f", adc_mult);
|
||||
}
|
||||
// Power management commands
|
||||
} else if (memcmp(config, "pwrmgt.support", 14) == 0) {
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
strcpy(reply, "> supported");
|
||||
#else
|
||||
strcpy(reply, "> unsupported");
|
||||
#endif
|
||||
} else if (memcmp(config, "pwrmgt.source", 13) == 0) {
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery");
|
||||
#else
|
||||
strcpy(reply, "ERROR: Power management not supported");
|
||||
#endif
|
||||
} else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) {
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
sprintf(reply, "> Reset: %s; Shutdown: %s",
|
||||
_board->getResetReasonString(_board->getResetReason()),
|
||||
_board->getShutdownReasonString(_board->getShutdownReason()));
|
||||
#else
|
||||
strcpy(reply, "ERROR: Power management not supported");
|
||||
#endif
|
||||
} else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) {
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
sprintf(reply, "> %u mV", _board->getBootVoltage());
|
||||
#else
|
||||
strcpy(reply, "ERROR: Power management not supported");
|
||||
#endif
|
||||
} else {
|
||||
sprintf(reply, "??: %s", config);
|
||||
}
|
||||
/*
|
||||
* SET commands
|
||||
*/
|
||||
} else if (memcmp(command, "set ", 4) == 0) {
|
||||
handleSetCmd(sender_timestamp, command, reply);
|
||||
const char* config = &command[4];
|
||||
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");
|
||||
#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);
|
||||
#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;
|
||||
if (freq >= 300.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, "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, "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;
|
||||
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");
|
||||
#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);
|
||||
}
|
||||
#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");
|
||||
#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 {
|
||||
sprintf(reply, "unknown config: %s", config);
|
||||
}
|
||||
} else if (sender_timestamp == 0 && strcmp(command, "erase") == 0) {
|
||||
bool s = _callbacks->formatFileSystem();
|
||||
sprintf(reply, "File system erase: %s", s ? "OK" : "Err");
|
||||
|
||||
@@ -33,7 +33,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;
|
||||
@@ -61,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 {
|
||||
|
||||
+277
-23
@@ -8,13 +8,126 @@
|
||||
|
||||
#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:
|
||||
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];
|
||||
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;
|
||||
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++) {
|
||||
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 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;
|
||||
}
|
||||
|
||||
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.
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (packet->_snr < _recent_repeater_min_snr_x4) {
|
||||
return;
|
||||
}
|
||||
setRecentRepeater(prefix, prefix_len, packet->_snr);
|
||||
}
|
||||
|
||||
public:
|
||||
SimpleMeshTables() {
|
||||
@@ -23,6 +136,11 @@ public:
|
||||
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;
|
||||
_recent_repeater_min_snr_x4 = -128;
|
||||
_recent_repeater_allow_fn = NULL;
|
||||
_recent_repeater_allow_ctx = NULL;
|
||||
}
|
||||
|
||||
#ifdef ESP32
|
||||
@@ -31,12 +149,16 @@ public:
|
||||
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,28 +166,8 @@ public:
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
_acks[_next_ack_idx] = ack;
|
||||
_next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS; // cyclic table
|
||||
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 (hasSeenAck(ack)) {
|
||||
if (packet->isRouteDirect()) {
|
||||
_direct_dups++; // keep some stats
|
||||
} else {
|
||||
@@ -73,13 +175,46 @@ public:
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
storeAck(ack);
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(&_hashes[_next_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE);
|
||||
_next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; // cyclic table
|
||||
uint8_t hash[MAX_HASH_SIZE];
|
||||
packet->calculatePacketHash(hash);
|
||||
|
||||
if (hasSeenHash(hash)) {
|
||||
if (packet->isRouteDirect()) {
|
||||
_direct_dups++; // keep some stats
|
||||
} else {
|
||||
_flood_dups++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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 +242,124 @@ public:
|
||||
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 = avgSnrX4RoundUp(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;
|
||||
}
|
||||
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) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 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 == 0) {
|
||||
continue;
|
||||
}
|
||||
if (prefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void resetStats() { _direct_dups = _flood_dups = 0; }
|
||||
};
|
||||
Reference in new issue
Block a user