Compare commits

...
Author SHA1 Message Date
mikecarper 7d237f8730 Update keymind retry controls 2026-05-08 16:57:03 -07:00
mikecarper ca0679e999 Merge adaptive CR direct retries into keymind 2026-05-08 12:43:24 -07:00
mikecarper bb8e7140a9 Add adaptive CR for direct retries 2026-05-08 12:37:08 -07:00
mikecarper c244effb52 Revise Halo and Keymind settings documentation
Updated settings and examples for Halo and Keymind configuration, including changes to retry settings and prefix handling.
2026-05-08 11:06:24 -07:00
mikecarper b475bde730 Unify retry preset settings 2026-05-08 10:53:21 -07:00
mikecarper ae67293bcc Honor flood retry ignore for echoes 2026-05-08 01:23:03 -07:00
mikecarper 3a6766b0fd Improve flood retry bridge buckets 2026-05-08 00:50:17 -07:00
mikecarper 9f81ffc692 Merge branch 'meshcore-dev:main' into halo-direct-path-retries 2026-05-07 23:50:11 -07:00
mikecarper 4e1002029c Merge branch 'meshcore-dev:main' into keymind 2026-05-07 23:49:57 -07:00
mikecarper 8dcd4975d5 Delay direct retry final failure 2026-05-07 16:55:37 -07:00
mikecarper e0b19a1f60 Fix direct retry diagnostics 2026-05-07 16:51:49 -07:00
mikecarper 93bd0f0869 Fix flood retry diagnostics 2026-05-07 16:40:31 -07:00
mikecarper d4febe7867 Add flood retry controls 2026-05-07 11:48:29 -07:00
mikecarper 674beb0e47 Improve repeater direct retry handling 2026-05-07 01:01:52 -07:00
mikecarper ea3f7acb4d Merge remote-tracking branch 'iotthinks/PowerSaving-v15' into halo-direct-path-retries
# Conflicts:
#	build.sh
#	src/helpers/CommonCLI.cpp
2026-05-06 15:37:48 -07:00
Kevin Le 3281ea2924 To drift forward less than 7s/day (3.5 minutes/month) for ESP32-based repeaters with Power Saving. 2026-05-06 15:07:59 +07:00
Kevin Le 517d3ed643 Added isReadBusy for NRF52 BLE 2026-05-06 12:40:52 +07:00
Kevin Le eac4f7201e Commented NRF52_POWER_MANAGEMENT and added PowerSaving BLE companions 2026-05-06 12:38:22 +07:00
Kevin Le 7f02cbe80b Delayed sleep for BLE companions if BLE read is busy. Reduced sleep period from 50 sticks to 10 sticks 2026-05-04 22:04:11 +07:00
mikecarper 6c20a1062f Refine direct retry SNR handling and recent repeater controls 2026-04-27 16:33:39 -07:00
Kevin Le e608503aa0 Merged latest changes from "Add CLI control to LoRa's fem LNA" https://github.com/meshcore-dev/MeshCore/pull/2140 2026-04-27 12:15:46 +07:00
Kevin Le fa9f3bd935 Added Heltec_Wireless_Tracker_companion_radio_ble_ps 2026-04-27 11:07:36 +07:00
Kevin Le 181ddb2268 Added Heltec_WSL3_companion_radio_ble_ps 2026-04-27 10:18:55 +07:00
mikecarper 8fadd31159 Merge branch 'main' of https://github.com/IoTThinks/MeshCore into halo-direct-path-retries 2026-04-26 19:28:16 -07:00
Kevin Le 5a45d54afe Added LilyGo_TBeam_1W_companion_radio_ble_ps 2026-04-26 16:16:43 +07:00
Kevin Le aa400da945 Clear stale wakeup sources to avoid ghost wakeup 2026-04-25 23:38:38 +07:00
Kevin Le a44137662e Added TimeTrim and Kept RTC 8M during sleep 2026-04-25 23:37:57 +07:00
mikecarper 0243ec41e8 Round up the SNR vs replacement to get a weighted average. 2026-04-24 15:19:48 -07:00
mikecarper 756268e2ee Fix issue with packet prefixes getting added to the table. 2026-04-24 14:41:07 -07:00
Kevin Le 0702065dd2 Delay sleep when BLE is active writing. 2026-04-24 23:05:31 +07:00
Kevin Le c5d8ada27d Added features from PowerSaving 14.1.1 2026-04-24 23:02:11 +07:00
mikecarper 9c2ac5aa1c Max retries is now a var that can be set between 1 to 15 2026-04-24 00:08:10 -07:00
mikecarper 577433ce47 Retry 3 times with a 200ms,300ms,400ms backoff. 2026-04-23 16:25:51 -07:00
mikecarper 6a53f6de27 Merge branch 'pr/buildsh-refactor-menu' into halo-direct-path-retries 2026-04-21 16:55:30 -07:00
mikecarper fae6dd83fd Merge branch 'main' into halo-direct-path-retries 2026-04-21 16:38:11 -07:00
mikecarper 8525b4e980 For packets with a path set; auto try again if no echo was heard 2026-04-21 16:24:00 -07:00
mikecarper d8552e3344 Better json arg passing in build.sh 2026-04-20 14:06:39 -07:00
mikecarper 4c393348df Add new line at the end of the file. 2026-04-20 13:53:38 -07:00
mikecarper 166f804da1 Remove unrelated CLI changes from PR branch 2026-04-20 13:47:01 -07:00
mikecarper 0ce1a55017 Refactor build.sh menu and build flow 2026-04-20 13:40:07 -07:00
mikecarper 00c14cd88c Add local-only uf2reset CLI command 2026-04-17 16:40:00 -07:00
94 changed files with 6398 additions and 373 deletions

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# ./build-iotthinks.sh
export FIRMWARE_VERSION="PowerSaving15"
############# Repeaters #############
# Commonly-used boards
## ESP32 - 12 boards
./build.sh build-firmware \
Heltec_v3_repeater \
Heltec_WSL3_repeater \
heltec_v4_repeater \
Station_G2_repeater \
T_Beam_S3_Supreme_SX1262_repeater \
Tbeam_SX1262_repeater \
LilyGo_T3S3_sx1262_repeater \
Xiao_S3_WIO_repeater \
Xiao_C3_repeater \
Xiao_C6_repeater_ \
Heltec_E290_repeater \
Heltec_Wireless_Tracker_repeater
## NRF52 - 13 boards
./build.sh build-firmware \
RAK_4631_repeater \
Heltec_t114_repeater \
Xiao_nrf52_repeater \
Heltec_mesh_solar_repeater \
ProMicro_repeater \
SenseCap_Solar_repeater \
t1000e_repeater \
LilyGo_T-Echo_repeater \
WioTrackerL1_repeater \
RAK_3401_repeater \
RAK_WisMesh_Tag_repeater \
GAT562_30S_Mesh_Kit_repeater \
GAT562_Mesh_Tracker_Pro_repeater
## ESP32, SX1276 - 3 boards
./build.sh build-firmware \
Heltec_v2_repeater \
LilyGo_TLora_V2_1_1_6_repeater \
Tbeam_SX1276_repeater
## Ikoka - 3 boards
./build.sh build-firmware \
ikoka_nano_nrf_22dbm_repeater \
ikoka_nano_nrf_30dbm_repeater \
ikoka_nano_nrf_33dbm_repeater
############# Room Server #############
# ESP32
./build.sh build-firmware \
Heltec_v3_room_server \
heltec_v4_room_server
# NRF52
./build.sh build-firmware \
RAK_4631_room_server \
Heltec_t114_room_server \
Xiao_nrf52_room_server \
t1000e_room_server \
WioTrackerL1_room_server \
RAK_3401_room_server
############# Companions BLE #############
# ESP32
./build.sh build-firmware \
Heltec_v3_companion_radio_ble_ps \
heltec_v4_companion_radio_ble_ps \
heltec_v4_companion_radio_ble_ps_femoff \
Xiao_S3_WIO_companion_radio_ble \
Heltec_Wireless_Paper_companion_radio_ble
# NRF52
./build.sh build-firmware \
RAK_4631_companion_radio_ble \
Heltec_t114_companion_radio_ble \
Xiao_nrf52_companion_radio_ble \
t1000e_companion_radio_ble \
LilyGo_T-Echo_companion_radio_ble \
WioTrackerL1_companion_radio_ble \
RAK_3401_companion_radio_ble \
RAK_WisMesh_Tag_companion_radio_ble
############# Companions USB #############
./build.sh build-firmware \
Heltec_v3_companion_radio_usb
############# Companions BLE PS #############
./build.sh build-firmware \
Heltec_v3_companion_radio_ble_ps \
heltec_v4_companion_radio_ble_ps \
heltec_v4_3_companion_radio_ble_ps_femoff \
Xiao_C3_companion_radio_ble_ps \
Xiao_S3_companion_radio_ble_ps \
Xiao_S3_WIO_companion_radio_ble_ps \
Heltec_v2_companion_radio_ble_ps
+630 -163
View File
@@ -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,86 @@ build_firmwares() {
build_room_server_firmwares
}
# clean build dir
rm -rf out
mkdir -p out
prepare_output_dir() {
local output_dir="$OUTPUT_DIR"
# handle script args
if [[ $1 == "build-firmware" ]]; then
TARGETS=${@:2}
if [ "$TARGETS" ]; then
for env in $TARGETS; do
build_firmware $env
done
else
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 "$@"
+243 -1
View File
@@ -115,6 +115,28 @@ 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 <page>`
- `get recent.repeater page <page>`
- `set recent.repeater <prefix_hex_6> <snr_db>`
**Parameters:**
- `prefix_hex_6`: Exactly 3 bytes of next-hop prefix in hex (6 chars)
- `snr_db`: SNR in dB (supports decimals; stored at x4 precision)
- `page`: 1-based page number
**Notes:**
- `set` stores or updates the prefix in the recent repeater table.
- Rows are sorted by prefix width (3-byte, 2-byte, 1-byte), then SNR descending.
- A full direct retry failure lowers the stored SNR by `0.25 dB`.
- If a full failure has no row yet, it first seeds the row at the active retry cutoff + `2.5 dB`, then applies the `0.25 dB` penalty.
- Serial CLI page size is fixed at `128` rows; choose page with `get recent.repeater <page>`.
- Over LoRa remote CLI, page size is fixed at `7` rows; choose page with `get recent.repeater <page>`.
---
## Statistics
### Clear Stats
@@ -277,6 +299,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore
---
#### View or change the LoRa FEM receive-path gain state on supported boards
**Usage:**
- `get radio.fem.rxgain`
- `set radio.fem.rxgain <state>`
**Parameters:**
- `state`: `on`|`off`
**Notes:**
- This controls the external LoRa FEM receive-path LNA where the board supports it.
- This is separate from `radio.rxgain`, which controls the radio chip receive gain mode.
---
### System
#### View or change this node's name
@@ -500,7 +536,116 @@ This document provides an overview of CLI commands that can be sent to MeshCore
**Parameters:**
- `value`: Direct transmit delay factor (0-2)
**Default:** `0.2`
**Default:** `0.3`
**Note:** Direct retry waits include the same airtime-based randomized delay calculation as direct retransmits, so this factor also controls retry echo windows.
---
#### View or change whether direct retries use the recent repeater blacklist
**Usage:**
- `get direct.retry.heard`
- `set direct.retry.heard <state>`
**Parameters:**
- `state`: `on`|`off`
**Default:** `on`
**Note:** When enabled, the recent repeater table is the only direct retry eligibility gate. Prefixes missing from the table are assumed reachable; prefixes in the table below the active SNR gate are blocked. Neighbor data is not used.
---
#### View or change adaptive coding rate for direct retry packets
**Usage:**
- `get direct.retry.cr`
- `set direct.retry.cr <cr4_min>,<cr5_min>,<cr7_min>,<cr8_max>`
- `set direct.retry.cr off`
**Parameters:**
- `cr4_min`: SNR in dB where retry packets use `CR4`
- `cr5_min`: SNR in dB where retry packets use `CR5`
- `cr7_min`: SNR in dB where retry packets use `CR7`
- `cr8_max`: SNR in dB where retry packets use `CR8`
**Default:** `10.0,7.5,2.5,2.5`
**Note:** DM retry packets use the next-hop SNR from a recent repeater table entry to pick a local transmit coding rate; if no recent entry is available, retry packets use `CR5`. With the default, SNR `10.0 dB` and up uses `CR4`, SNR `7.5 dB` and up uses `CR5`, SNR `2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. `CR6` is never selected. Use `set direct.retry.cr off` to disable adaptive coding-rate overrides. If adaptive selection chooses `CR4`, retries after the third attempt use `CR5`.
---
#### View or change the SNR margin used for direct retry eligibility
**Usage:**
- `get direct.retry.margin`
- `set direct.retry.margin <value>`
**Parameters:**
- `value`: Rooftop preset margin in dB above the SF-specific receive floor (minimum `0`, maximum `40`, quarter-dB precision, default `5.0`)
**Default:** `5.0`
**Note:** `get direct.retry.margin` returns the active preset's effective margin. The retry gate uses the active SF floor of `SF5=-2.5`, `SF6=-5`, `SF7=-7.5`, `SF8=-10`, `SF9=-12.5`, `SF10=-15`, `SF11=-17.5`, `SF12=-20`, then adds this margin.
---
#### View or change the retry preset
**Usage:**
- `get retry.preset`
- `set retry.preset <value>`
**Parameters:**
- `value`: `infra`|`rooftop`|`mobile` or `0`|`1`|`2`
**Default:** `rooftop` (`1`)
**Presets:**
- `infra` (`0`): `275 ms` direct base wait, `4` direct retries, `150 ms` added per direct retry, SNR gate is SF floor + `15 dB`; flood retry defaults to `1` retry and path gate `1`
- `rooftop` (`1`): `175 ms` direct base wait, `15` direct retries, `100 ms` added per direct retry, SNR gate is SF floor + `5 dB`; flood retry defaults to `3` retries and path gate `2`
- `mobile` (`2`): `175 ms` direct base wait, `15` direct retries, `50 ms` added per direct retry, SNR gate is the SF floor; flood retry defaults to `3` retries and path gate `1`
**Note:** Selecting a preset copies those values into the direct retry settings and resets flood retry defaults. You can refine `direct.retry.margin`, `direct.retry.count`, `direct.retry.base`, `direct.retry.step`, `flood.retry.count`, or `flood.retry.path` afterward. Retry delay is `direct.txdelay` jitter + base wait + packet-length airtime wait + per-attempt step.
---
#### View or change the number of direct retry attempts
**Usage:**
- `get direct.retry.count`
- `set direct.retry.count <value>`
**Parameters:**
- `value`: Maximum retry attempts after initial TX (`1`-`15`)
**Default:** `15`
**Note:** The effective value is capped by total direct path length: paths of `3` hops or less use at most `8` retries, `4` hops use at most `12`, and `5+` hops use at most `15`. A queued resend is canceled early when the next-hop echo is heard.
---
#### View or change the base direct retry wait (milliseconds)
**Usage:**
- `get direct.retry.base`
- `set direct.retry.base <value>`
**Parameters:**
- `value`: Base wait in milliseconds (`10`-`5000`)
**Default:** `175`
**Note:** The configured base is added to packet-length airtime and `direct.txdelay` jitter. Preset defaults are already reduced to account for the added `direct.txdelay` component.
---
#### View or change the direct retry per-attempt add time (milliseconds)
**Usage:**
- `get direct.retry.step`
- `set direct.retry.step <value>`
**Parameters:**
- `value`: Milliseconds added per retry attempt (`0`-`5000`)
**Default:** `100`
**Note:** This controls the linear add after the first retry wait. For example, `base=300` and `step=150` adds `0`, `150`, `300`, ... ms across retry attempts.
---
@@ -627,6 +772,103 @@ This document provides an overview of CLI commands that can be sent to MeshCore
---
#### View or change the number of flood retry attempts
**Usage:**
- `get flood.retry.count`
- `set flood.retry.count <value>`
**Parameters:**
- `value`: Maximum retry attempts after initial flood TX (`0`-`15`)
**Default:** `3` for `rooftop` and `mobile`, `1` for `infra`
**Note:** `0` disables flood retry.
---
#### View or change the flood retry path gate
**Usage:**
- `get flood.retry.path`
- `set flood.retry.path <value>`
**Parameters:**
- `value`: Maximum flood path length eligible for retry (`0`-`63`), or `off` to disable the gate
**Default:** `2` for `rooftop`, `1` for `infra` and `mobile`
---
#### View or change whether advert packets are flood-retried
**Usage:**
- `get flood.retry.advert`
- `set flood.retry.advert <state>`
**Parameters:**
- `state`: `on` or `off`
**Default:** `off`
**Note:** When this is `off`, node advert packets (`PAYLOAD_TYPE_ADVERT`, type `4`) are not queued for flood retry.
---
#### View or change flood retry target prefixes
**Usage:**
- `get flood.retry.prefixes`
- `set flood.retry.prefixes <prefixes>`
**Parameters:**
- `prefixes`: Comma-separated 3-byte hex prefixes, such as `A1B2C3,D4E5F6`; use `none` or `off` to clear
**Default:** `none`
**Note:** Prefixes are stored as 3 bytes. Flood retry skips packets whose path already contains a matching target prefix. When prefixes are configured, only a downstream echo from one of those target prefixes cancels a queued retry; when no prefixes are configured, any downstream echo cancels it. Matching works with 3-byte, 2-byte, or 1-byte flood paths by comparing the matching leading bytes.
---
#### View or change flood retry bridge mode
**Usage:**
- `get flood.retry.bridge`
- `set flood.retry.bridge <state>`
**Parameters:**
- `state`: `on` or `off`
**Default:** `off`
**Note:** Bridge mode uses bucket definitions instead of the single `flood.retry.prefixes` target list. It also has an implicit unconfigured catch-all bucket. If a flood comes from one fresh configured bucket, retry continues until every other fresh configured bucket plus the catch-all bucket has been heard or `flood.retry.count` is exhausted. If a flood comes from an unconfigured or pathless source, retry targets every fresh configured bucket. This means one configured bucket bridges between that bucket and everything else. Prefixes in `flood.retry.ignore` never count as heard bridge targets.
---
#### View or change flood retry bridge buckets
**Usage:**
- `get flood.retry.bucket.<bucket>`
- `set flood.retry.bucket <bucket> <prefixes>`
**Parameters:**
- `bucket`: Bucket number (`1`-`6`)
- `prefixes`: Up to 17 comma-separated 3-byte hex prefixes, such as `AABBCC,223344`; use `none` or `off` to clear
**Default:** all buckets empty
**Note:** Prefixes are stored as 3 bytes but match 3-byte, 2-byte, and 1-byte flood paths by comparing leading bytes. Bucket prefixes are included in bridge retry logic only if they were heard in the recent repeater table within the last hour.
---
#### View or change flood retry ignored prefixes
**Usage:**
- `get flood.retry.ignore`
- `set flood.retry.ignore <prefixes>`
**Parameters:**
- `prefixes`: Up to 8 comma-separated 3-byte hex prefixes, such as `AABBCC,223344`; use `none` or `off` to clear
**Default:** empty
**Note:** In non-bridge retry, an echo whose last hop matches an ignored prefix does not cancel a queued retry as successful. In bridge mode, ignored prefixes do not count as a heard bridge bucket or as the implicit catch-all bucket when bridge retry decides whether every target has repeated the flood.
---
### ACL
#### Add, update or remove permissions for a companion
+290
View File
@@ -0,0 +1,290 @@
# Halo and Keymind Branch Settings
This file covers only CLI settings added by the Halo or Keymind branches. Use
`docs/cli_commands.md` for the general MeshCore CLI.
## Quick Start
```text
set retry.preset rooftop
set direct.retry.heard on
set flood.retry.advert off
set flood.retry.bridge off
set flood.retry.prefixes none
set flood.retry.ignore none
```
Then verify:
```text
get retry.preset
get direct.retry.heard
get flood.retry.advert
get flood.retry.prefixes
get flood.retry.ignore
```
Use prefixes from the analyzer or neighbors list or `get recent.repeater` after the repeater has been online for a few hours.
## Common Examples
Disable retrying advert packets:
```text
set flood.retry.advert off
get flood.retry.advert
```
Ignore a repeater as a successful flood retry echo:
Use this if you have a car repeater and a house repeater; have the house ignore the car.
```text
set flood.retry.ignore 71CE82,C7618C
get flood.retry.ignore
```
Only accept specific downstream relays as flood retry success:
You're in a hole and need to hit a mountain top repeater to get out; keep trying till one you see one of these send out your packet.
```text
set flood.retry.prefixes A58296,860CCA,425E5C
get flood.retry.prefixes
```
Bridge two groups of repeaters:
```text
set flood.retry.bridge on
set flood.retry.bucket 1 71CE82,C7618C
set flood.retry.bucket 2 BEEBB0,425E5C
get flood.retry.bucket.1
get flood.retry.bucket.2
```
Return to simple non-bridge flood retry:
```text
set flood.retry.bridge off
set flood.retry.prefixes none
set flood.retry.ignore none
```
## Added Settings
| Setting | What it does | How to use | Example |
| --- | --- | --- | --- |
| `recent.repeater` | Shows or seeds the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. | `get recent.repeater`, `get recent.repeater <page>`, `set recent.repeater <prefix> <snr_db>` | `set recent.repeater A1B2C3 -8.5` |
| `radio.fem.rxgain` | Controls the external LoRa FEM receive-path LNA where the board supports it. | `get radio.fem.rxgain`, `set radio.fem.rxgain on/off` | `set radio.fem.rxgain on` |
## Recent Repeater Table
Direct retry uses the recent repeater table when `direct.retry.heard` is `on`.
Bridge buckets also use this table: a configured bucket prefix is active only
when it was heard within the last hour.
Show learned rows:
```text
get recent.repeater
get recent.repeater 2
```
Seed or correct a prefix:
```text
set recent.repeater A1B2C3 8.5
```
Rows are sorted by prefix width, then SNR. A full direct retry failure lowers
the matching row by `0.25 dB`.
## Direct Retry Settings
Direct retry applies to direct-routed packets. A queued resend is canceled when the next-hop echo is heard.
| Setting | What it does | How to use | Example |
| --- | --- | --- | --- |
| `retry.preset` | Applies shared direct and flood retry defaults. Values: `infra`, `rooftop`, `mobile` or `0`, `1`, `2`. | `get retry.preset`, `set retry.preset <value>` | `set retry.preset rooftop` |
| `direct.retry.heard` | Uses the recent repeater table as the direct retry eligibility gate. | `get direct.retry.heard`, `set direct.retry.heard on/off` | `set direct.retry.heard on` |
| `direct.retry.margin` | SNR margin in dB above the SF-specific receive floor. | `get direct.retry.margin`, `set direct.retry.margin <0-40>` | `set direct.retry.margin 5` |
| `direct.retry.count` | Maximum direct retry attempts after initial TX. | `get direct.retry.count`, `set direct.retry.count <1-15>` | `set direct.retry.count 15` |
| `direct.retry.base` | Base wait in milliseconds before retry. | `get direct.retry.base`, `set direct.retry.base <10-5000>` | `set direct.retry.base 175` |
| `direct.retry.step` | Milliseconds added per retry attempt. | `get direct.retry.step`, `set direct.retry.step <0-5000>` | `set direct.retry.step 100` |
| `direct.retry.cr` | Adaptive coding-rate thresholds for direct retry packets. Uses `CR4`, `CR5`, `CR7`, or `CR8`; `CR6` is never selected. | `get direct.retry.cr`, `set direct.retry.cr <cr4_min>,<cr5_min>,<cr7_min>,<cr8_max>`, `set direct.retry.cr off` | `set direct.retry.cr 10.0,7.5,2.5,0` |
The default adaptive coding-rate profile is `10.0,7.5,2.5,2.5`.
SNR `10.0 dB` and up uses `CR4`, `7.5 dB` and up uses `CR5`,
`2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. If no
recent repeater table entry is available, retry packets use `CR5`. Use
`set direct.retry.cr off` to disable adaptive coding-rate overrides. If
adaptive selection chooses `CR4`, retries after the third attempt use
`CR5`.
Preset details:
| Preset | Base | Count | Step | SNR gate |
| --- | ---: | ---: | ---: | --- |
| `infra` | `275 ms` | `4` | `150 ms` | SF floor + `15 dB` |
| `rooftop` | `175 ms` | `15` | `100 ms` | SF floor + `5 dB` |
| `mobile` | `175 ms` | `15` | `50 ms` | SF floor |
Example for a quiet fixed repeater:
```text
set retry.preset rooftop
set direct.retry.heard on
set direct.retry.margin 5
```
Example for a moving or weak-link node:
```text
set retry.preset mobile
set direct.retry.margin 0
```
## Flood And Advert Settings
Flood retry applies to flood-routed packets. A queued retry is canceled when a qualifying downstream echo is heard.
| Setting | What it does | How to use | Example |
| --- | --- | --- | --- |
| `flood.retry.count` | Maximum flood retry attempts after initial TX. `0` disables flood retry. | `get flood.retry.count`, `set flood.retry.count <0-15>` | `set flood.retry.count 7` |
| `flood.retry.path` | Maximum path hash count eligible for flood retry, or `off` to disable the gate. | `get flood.retry.path`, `set flood.retry.path <0-63/off>` | `set flood.retry.path 1` |
| `flood.retry.advert` | Allows or blocks retry for node advert packets (`type=4`). Default is `off`. | `get flood.retry.advert`, `set flood.retry.advert on/off` | `set flood.retry.advert off` |
| `flood.retry.prefixes` | Target prefixes. If set, only matching downstream echoes cancel a retry. | `get flood.retry.prefixes`, `set flood.retry.prefixes <prefixes/none/off>` | `set flood.retry.prefixes BEEBB0,425E5C` |
| `flood.retry.ignore` | Ignored prefixes. In non-bridge retry, ignored last-hop echoes do not cancel retry. | `get flood.retry.ignore`, `set flood.retry.ignore <prefixes/none/off>` | `set flood.retry.ignore 71CE82,C7618C` |
| `flood.retry.bridge` | Enables bucket-based bridge retry logic. | `get flood.retry.bridge`, `set flood.retry.bridge on/off` | `set flood.retry.bridge on` |
| `flood.retry.bucket.<n>` | Shows one bridge bucket. Buckets are numbered `1`-`6`. | `get flood.retry.bucket.<n>` | `get flood.retry.bucket.1` |
| `flood.retry.bucket` | Sets bridge bucket prefixes. | `set flood.retry.bucket <1-6> <prefixes/none/off>` | `set flood.retry.bucket 1 71CE82,C7618C` |
The shared retry preset sets these flood defaults:
| Preset | Retry count | Path gate |
| --- | ---: | ---: |
| `infra` | `1` | `1` |
| `rooftop` | `3` | `2` |
| `mobile` | `3` | `1` |
Example for path-gated retry:
```text
set retry.preset rooftop
set flood.retry.path 1
set flood.retry.advert off
set flood.retry.ignore 71CE82,C7618C
```
## North South Buckets
Buckets describe groups of repeaters on different sides of this relay. Bucket
numbers do not have built-in meanings; this example uses bucket `1` for North
and bucket `2` for South.
```text
North bucket 1
+-----------------------+
| A1B2C3 D4E5F6 |
| North A North B |
+-----------+-----------+
|
v
+-----------+
| This node |
+-----------+
^
|
+-----------+-----------+
| 71CE82 C7618C |
| South A South B |
+-----------------------+
South bucket 2
```
Configure the buckets:
```text
set flood.retry.bridge on
set flood.retry.bucket 1 A1B2C3,D4E5F6
set flood.retry.bucket 2 71CE82,C7618C
set flood.retry.ignore none
```
Packet heard from the North:
```text
heard source
|
v
+--------------+ retry targets
| North bucket | -----> South bucket
| bucket 1 | -----> Other fresh/unbucketed relays
+--------------+
```
Packet heard from the South:
```text
heard source
|
v
+--------------+ retry targets
| South bucket | -----> North bucket
| bucket 2 | -----> Other fresh/unbucketed relays
+--------------+
```
Packet heard from an unbucketed or pathless source:
```text
heard source
|
v
+--------------+ retry targets
| Other bucket | -----> North bucket
| implicit | -----> South bucket
+--------------+
```
Bridge retry stays eligible until every target bucket has been heard or
`flood.retry.count` is exhausted. A configured bucket is a target only when at
least one of its prefixes is fresh in `recent.repeater`. Prefixes in
`flood.retry.ignore` never count as bucket hits.
## Troubleshooting
If advert packets are still retrying:
```text
get flood.retry.advert
set flood.retry.advert off
```
If ignored prefixes still appear in `flood retry good` logs:
```text
get flood.retry.ignore
set flood.retry.ignore <prefix>
```
The ignored prefix must match the last hop shown as `heard=<prefix>`. For example, this log needs `C7618C` in the ignore list:
```text
flood retry good (... path=7773D0>C7618C, heard=C7618C ...)
```
If retries are too aggressive:
```text
set flood.retry.count 1
set flood.retry.path 1
set direct.retry.count 4
```
If retries are too sparse:
```text
set flood.retry.count 7
set flood.retry.path 2
```
+2
View File
@@ -233,6 +233,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89
file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90
file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121
file.read((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122
file.close();
}
@@ -273,6 +274,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89
file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90
file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121
file.write((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122
file.close();
}
+37
View File
@@ -61,6 +61,8 @@
#define CMD_SEND_CHANNEL_DATA 62
#define CMD_SET_DEFAULT_FLOOD_SCOPE 63
#define CMD_GET_DEFAULT_FLOOD_SCOPE 64
#define CMD_GET_RADIO_FEM_RXGAIN 65
#define CMD_SET_RADIO_FEM_RXGAIN 66
// Stats sub-types for CMD_GET_STATS
#define STATS_TYPE_CORE 0
@@ -876,6 +878,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_prefs.rx_boosted_gain = 1; // enabled by default
#endif
#endif
_prefs.radio_fem_rxgain = 1;
}
void MyMesh::begin(bool has_display) {
@@ -925,6 +928,7 @@ void MyMesh::begin(bool has_display) {
_prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER);
_prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1
_prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours
_prefs.radio_fem_rxgain = constrain(_prefs.radio_fem_rxgain, 0, 1);
#ifdef BLE_PIN_CODE // 123456 by default
if (_prefs.ble_pin == 0) {
@@ -954,6 +958,7 @@ void MyMesh::begin(bool has_display) {
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
}
@@ -1798,6 +1803,30 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
}
} else if (cmd_frame[0] == CMD_GET_RADIO_FEM_RXGAIN) {
if (!board.canControlLoRaFemLna()) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
} else {
out_frame[0] = RESP_CODE_OK;
uint8_t value = board.isLoRaFemLnaEnabled() ? 1 : 0;
memcpy(&out_frame[1], &value, 1);
_serial->writeFrame(out_frame, 2);
}
} else if (cmd_frame[0] == CMD_SET_RADIO_FEM_RXGAIN && len >= 2) {
uint8_t value = cmd_frame[1];
if (!board.canControlLoRaFemLna()) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
} else if (value <= 1) {
_prefs.radio_fem_rxgain = value;
if (board.setLoRaFemLnaEnabled(value != 0)) {
savePrefs();
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
}
} else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
}
} else if (cmd_frame[0] == CMD_GET_ADVERT_PATH && len >= PUB_KEY_SIZE+2) {
// FUTURE use: uint8_t reserved = cmd_frame[1];
uint8_t *pub_key = &cmd_frame[2];
@@ -2191,3 +2220,11 @@ bool MyMesh::advert() {
return false;
}
}
// To check if there is pending work
bool MyMesh::hasPendingWork() const {
#if defined(WITH_BRIDGE)
if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep
#endif
return _mgr->getOutboundTotal() > 0;
}
+4 -1
View File
@@ -165,7 +165,7 @@ protected:
public:
void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); }
#if ENV_INCLUDE_GPS == 1
void applyGpsPrefs() {
sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0");
@@ -177,6 +177,9 @@ public:
}
#endif
// To check if there is pending work
bool hasPendingWork() const;
private:
void writeOKFrame();
void writeErrFrame(uint8_t err_code);
+2 -1
View File
@@ -29,9 +29,10 @@ struct NodePrefs { // persisted to file
uint32_t gps_interval; // GPS read interval in seconds
uint8_t autoadd_config; // bitmask for auto-add contacts config
uint8_t rx_boosted_gain; // SX126x RX boosted gain mode (0=power saving, 1=boosted)
uint8_t radio_fem_rxgain; // LoRa FEM RX gain setting
uint8_t client_repeat;
uint8_t path_hash_mode; // which path mode to use when sending
uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
char default_scope_name[31];
uint8_t default_scope_key[16];
};
};
+44 -2
View File
@@ -2,6 +2,11 @@
#include <Mesh.h>
#include "MyMesh.h"
#ifdef ESP32_PLATFORM
#include "esp_pm.h"
#include "esp_bt.h"
#endif
// Believe it or not, this std C function is busted on some platforms!
static uint32_t _atoi(const char* sp) {
uint32_t n = 0;
@@ -42,8 +47,8 @@ static uint32_t _atoi(const char* sp) {
#define TCP_PORT 5000
#endif
#elif defined(BLE_PIN_CODE)
#include <helpers/esp32/SerialBLEInterface.h>
SerialBLEInterface serial_interface;
#include <helpers/esp32/SerialBLEInterface.h>
SerialBLEInterface serial_interface;
#elif defined(SERIAL_RX)
#include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface serial_interface;
@@ -220,6 +225,33 @@ void setup() {
#ifdef DISPLAY_CLASS
ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved
#endif
#ifdef ESP32_PLATFORM
// Enable BLE sleep
esp_err_t errBLESleep = esp_bt_sleep_enable();
if (errBLESleep == ESP_OK) {
Serial.println("Bluetooth sleep enabled successfully");
} else {
Serial.printf("Bluetooth sleep enable failed: %s\n", esp_err_to_name(errBLESleep));
}
#if CONFIG_IDF_TARGET_ESP32C3
esp_pm_config_esp32c3_t pm_config;
#elif CONFIG_IDF_TARGET_ESP32S3
esp_pm_config_esp32s3_t pm_config;
#elif CONFIG_IDF_TARGET_ESP32
esp_pm_config_esp32_t pm_config;
#endif
// Configure Power Management
pm_config = { .max_freq_mhz = 80, .min_freq_mhz = 40, .light_sleep_enable = true };
esp_err_t errPM = esp_pm_configure(&pm_config);
if (errPM == ESP_OK) {
Serial.println("Power Management configured successfully");
} else {
Serial.printf("Power Management failed to configure: %d\r\n", errPM);
}
#endif
}
void loop() {
@@ -229,4 +261,14 @@ void loop() {
ui_task.loop();
#endif
rtc_clock.tick();
if (!the_mesh.hasPendingWork()) {
#if defined(NRF52_PLATFORM)
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
#else if defined(ESP32_PLATFORM)
if (!serial_interface.isReadBusy() && !serial_interface.isWriteBusy()) { // BLE is not busy
vTaskDelay(pdMS_TO_TICKS(10)); // attempt to sleep
}
#endif
}
}
File diff suppressed because it is too large. Load diff
+49
View File
@@ -99,6 +99,15 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
RegionEntry* recv_pkt_region;
TransportKey default_scope;
RateLimiter discover_limiter, anon_limiter;
struct FloodRetryBridgeState {
uint8_t key[MAX_HASH_SIZE];
uint8_t source_bucket;
uint8_t target_mask;
uint8_t heard_mask;
uint8_t progress_marker;
bool active;
};
mutable FloodRetryBridgeState flood_retry_bridge_states[MAX_FLOOD_RETRY_SLOTS];
uint32_t pending_discover_tag;
unsigned long pending_discover_until;
bool region_load_active;
@@ -110,8 +119,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 +131,30 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
ESPNowBridge bridge;
#endif
bool extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const;
int8_t getDirectRetryMinSNRX4() const;
uint8_t getDirectRetryCodingRateForSNR(int8_t snr_x4) const;
uint8_t getDirectRetryPreset() const;
uint8_t getDirectRetryConfiguredMaxAttempts() const;
uint32_t getDirectRetryAttemptStepMillis() const;
bool hasFloodRetryPrefixes() const;
bool floodRetryPrefixMatches(const mesh::Packet* packet) const;
bool floodRetryLastHopMatches(const mesh::Packet* packet) const;
bool floodRetryPrefixIgnored(const uint8_t* prefix, uint8_t prefix_len) const;
bool floodRetryPrefixFresh(const uint8_t* prefix, uint8_t prefix_len) const;
int floodRetryBucketForPrefix(const uint8_t* prefix, uint8_t prefix_len, bool require_fresh,
bool include_other) const;
int floodRetryBucketForPathHop(const uint8_t* prefix, uint8_t prefix_len, uint8_t hop,
uint8_t progress_marker) const;
int floodRetrySourceBucket(const mesh::Packet* packet) const;
uint8_t floodRetryBridgeTargetMask(uint8_t source_bucket) const;
uint8_t floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t source_bucket,
uint8_t progress_marker) const;
FloodRetryBridgeState* floodRetryBridgeStateFor(const mesh::Packet* packet, bool create) const;
void clearFloodRetryBridgeState(const mesh::Packet* packet);
void refreshFloodRetryHeardRecent(const mesh::Packet* packet);
void formatFloodRetryPath(char* dest, size_t dest_len, const mesh::Packet* packet) const;
bool formatFloodRetryHeard(char* dest, size_t dest_len, const mesh::Packet* packet) const;
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
@@ -146,6 +182,19 @@ protected:
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
uint8_t getDefaultTxCodingRate() const override { return active_cr; }
bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override;
void configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) override;
uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override;
uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override;
uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override;
void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override;
bool allowFloodRetry(const mesh::Packet* packet) const override;
void onFloodRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override;
bool hasFloodRetryTargetPrefix(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxPathLength(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxAttempts(const mesh::Packet* packet) const override;
bool isFloodRetryEchoTarget(const mesh::Packet* packet, uint8_t progress_marker) const override;
int getInterferenceThreshold() const override {
return _prefs.interference_threshold;
+2 -1
View File
@@ -41,7 +41,8 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi
}
// v1.2.3 (1 Jan 2025)
sprintf(_version_info, "%s (%s)", version, build_date);
snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date);
free(version);
}
void UITask::renderCurrScreen() {
+6 -14
View File
@@ -20,8 +20,7 @@ void halt() {
static char command[160];
// For power saving
unsigned long lastActive = 0; // mark last active time
unsigned long nextSleepinSecs = 120; // next sleep in seconds. The first sleep (if enabled) is after 2 minutes from boot
unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot
#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_)
static unsigned long userBtnDownAt = 0;
@@ -40,9 +39,6 @@ void setup() {
delay(5000);
#endif
// For power saving
lastActive = millis(); // mark last active time since boot
#ifdef DISPLAY_CLASS
if (display.begin()) {
display.startFrame();
@@ -155,16 +151,12 @@ void loop() {
rtc_clock.tick();
if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) {
#if defined(NRF52_PLATFORM)
#if defined(NRF52_PLATFORM)
board.sleep(1800); // nrf ignores seconds param, sleeps whenever possible
#else
if (the_mesh.millisHasNowPassed(lastActive + nextSleepinSecs * 1000)) { // To check if it is time to sleep
board.sleep(1800); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet
lastActive = millis();
nextSleepinSecs = 5; // Default: To work for 5s and sleep again
} else {
nextSleepinSecs += 5; // When there is pending work, to work another 5s
#else
if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep
board.sleep(30); // Sleep. Wake up after some seconds or when receiving a LoRa packet
}
#endif
#endif
}
}
+10
View File
@@ -652,6 +652,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.gps_enabled = 0;
_prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
_prefs.radio_fem_rxgain = 1;
next_post_idx = 0;
next_client_idx = 0;
@@ -693,6 +694,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
updateAdvertTimer();
updateFloodAdvertTimer();
@@ -1022,3 +1024,11 @@ void MyMesh::loop() {
uptime_millis += now - last_millis;
last_millis = now;
}
// To check if there is pending work
bool MyMesh::hasPendingWork() const {
#if defined(WITH_BRIDGE)
if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep
#endif
return _mgr->getOutboundTotal() > 0;
}
+3
View File
@@ -222,4 +222,7 @@ public:
void clearStats() override;
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
void loop();
// To check if there is pending work
bool hasPendingWork() const;
};
+2 -1
View File
@@ -41,7 +41,8 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi
}
// v1.2.3 (1 Jan 2025)
sprintf(_version_info, "%s (%s)", version, build_date);
snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date);
free(version);
}
void UITask::renderCurrScreen() {
+13
View File
@@ -18,6 +18,9 @@ void halt() {
static char command[MAX_POST_TEXT_LEN+1];
// For power saving
unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot
void setup() {
Serial.begin(115200);
delay(1000);
@@ -113,4 +116,14 @@ void loop() {
ui_task.loop();
#endif
rtc_clock.tick();
if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) {
#if defined(NRF52_PLATFORM)
board.sleep(1800); // nrf ignores seconds param, sleeps whenever possible
#else
if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep
board.sleep(1800); // Sleep. Wake up after 30 minutes or when receiving a LoRa packet
}
#endif
}
}
+2
View File
@@ -731,6 +731,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
_prefs.gps_enabled = 0;
_prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
_prefs.radio_fem_rxgain = 1;
memset(default_scope.key, 0, sizeof(default_scope.key));
}
@@ -766,6 +767,7 @@ void SensorMesh::begin(FILESYSTEM* fs) {
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
updateAdvertTimer();
updateFloodAdvertTimer();
+2 -1
View File
@@ -41,7 +41,8 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi
}
// v1.2.3 (1 Jan 2025)
sprintf(_version_info, "%s (%s)", version, build_date);
snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date);
free(version);
}
void UITask::renderCurrScreen() {
+45 -7
View File
@@ -52,6 +52,13 @@ void Dispatcher::updateTxBudget() {
}
}
void Dispatcher::restoreOutboundCodingRate() {
if (outbound_restore_cr != 0) {
_radio->setCodingRate(outbound_restore_cr);
outbound_restore_cr = 0;
}
}
int Dispatcher::calcRxDelay(float score, uint32_t air_time) const {
return (int) ((pow(10, 0.85f - score) - 1.0) * air_time);
}
@@ -105,7 +112,9 @@ void Dispatcher::loop() {
}
_radio->onSendFinished();
restoreOutboundCodingRate();
logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
onSendComplete(outbound);
if (outbound->isRouteFlood()) {
n_sent_flood++;
} else {
@@ -117,7 +126,9 @@ void Dispatcher::loop() {
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime());
_radio->onSendFinished();
restoreOutboundCodingRate();
logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
onSendFail(outbound);
releasePacket(outbound); // return to pool
outbound = NULL;
@@ -148,6 +159,7 @@ void Dispatcher::loop() {
bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) {
int i = 0;
pkt->tx_cr = 0;
pkt->header = raw[i++];
if (pkt->getPayloadVer() > PAYLOAD_VER_1) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): unsupported packet version", getLogDateTime());
@@ -267,7 +279,10 @@ void Dispatcher::processRecvPacket(Packet* pkt) {
uint8_t priority = (action >> 24) - 1;
uint32_t _delay = action & 0xFFFFFF;
_mgr->queueOutbound(pkt, priority, futureMillis(_delay));
if (!queueOutboundPacket(pkt, priority, _delay)) {
onSendFail(pkt);
releasePacket(pkt);
}
}
}
@@ -318,18 +333,32 @@ void Dispatcher::checkSend() {
if (len + outbound->payload_len > MAX_TRANS_UNIT) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): FATAL: Invalid packet queued... too long, len=%d", getLogDateTime(), len + outbound->payload_len);
_mgr->free(outbound);
onSendFail(outbound);
releasePacket(outbound);
outbound = NULL;
} else {
memcpy(&raw[len], outbound->payload, outbound->payload_len); len += outbound->payload_len;
uint32_t max_airtime = _radio->getEstAirtimeFor(len)*3/2;
outbound_restore_cr = 0;
uint8_t default_cr = getDefaultTxCodingRate();
if (outbound->tx_cr >= 4 && outbound->tx_cr <= 8 && default_cr >= 4 && default_cr <= 8
&& outbound->tx_cr != default_cr) {
if (_radio->setCodingRate(outbound->tx_cr)) {
outbound_restore_cr = default_cr;
max_airtime = _radio->getEstAirtimeFor(len)*3/2;
} else {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): WARN: failed to set packet CR%d", getLogDateTime(), (uint32_t)outbound->tx_cr);
}
}
outbound_start = _ms->getMillis();
bool success = _radio->startSendRaw(raw, len);
if (!success) {
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): ERROR: send start failed!", getLogDateTime());
restoreOutboundCodingRate();
logTxFail(outbound, outbound->getRawLength());
onSendFail(outbound);
releasePacket(outbound); // return to pool
outbound = NULL;
@@ -359,6 +388,7 @@ Packet* Dispatcher::obtainNewPacket() {
} else {
pkt->payload_len = pkt->path_len = 0;
pkt->_snr = 0;
pkt->tx_cr = 0;
}
return pkt;
}
@@ -367,13 +397,21 @@ void Dispatcher::releasePacket(Packet* packet) {
_mgr->free(packet);
}
void Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) {
bool Dispatcher::queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) {
if (!Packet::isValidPathLen(packet->path_len) || packet->payload_len > MAX_PACKET_PAYLOAD) {
MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): ERROR: invalid packet... path_len=%d, payload_len=%d", getLogDateTime(), (uint32_t) packet->path_len, (uint32_t) packet->payload_len);
_mgr->free(packet);
} else {
_mgr->queueOutbound(packet, priority, futureMillis(delay_millis));
return false;
}
return _mgr->queueOutbound(packet, priority, futureMillis(delay_millis));
}
bool Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) {
if (!queueOutboundPacket(packet, priority, delay_millis)) {
onSendFail(packet);
releasePacket(packet);
return false;
}
return true;
}
// Utility function -- handles the case where millis() wraps around back to zero
@@ -386,4 +424,4 @@ unsigned long Dispatcher::futureMillis(int millis_from_now) const {
return _ms->getMillis() + millis_from_now;
}
}
}
+16 -2
View File
@@ -46,6 +46,12 @@ public:
*/
virtual bool startSendRaw(const uint8_t* bytes, int len) = 0;
/**
* \brief Sets LoRa coding rate for subsequent transmits/receives.
* \returns true if the radio accepted the coding rate.
*/
virtual bool setCodingRate(uint8_t cr) { return false; }
/**
* \returns true if the previous 'startSendRaw()' completed successfully.
*/
@@ -87,7 +93,7 @@ public:
virtual Packet* allocNew() = 0;
virtual void free(Packet* packet) = 0;
virtual void queueOutbound(Packet* packet, uint8_t priority, uint32_t scheduled_for) = 0;
virtual bool queueOutbound(Packet* packet, uint8_t priority, uint32_t scheduled_for) = 0;
virtual Packet* getNextOutbound(uint32_t now) = 0; // by priority
virtual int getOutboundCount(uint32_t now) const = 0;
virtual int getOutboundTotal() const = 0;
@@ -116,6 +122,7 @@ typedef uint32_t DispatcherAction;
class Dispatcher {
Packet* outbound; // current outbound packet
unsigned long outbound_expiry, outbound_start, total_air_time, rx_air_time;
uint8_t outbound_restore_cr;
unsigned long next_tx_time;
unsigned long cad_busy_start;
unsigned long radio_nonrx_start;
@@ -128,6 +135,7 @@ class Dispatcher {
unsigned long duty_cycle_window_ms;
void processRecvPacket(Packet* pkt);
void restoreOutboundCodingRate();
void updateTxBudget();
protected:
@@ -140,6 +148,7 @@ protected:
: _radio(&radio), _ms(&ms), _mgr(&mgr)
{
outbound = NULL;
outbound_restore_cr = 0;
total_air_time = rx_air_time = 0;
next_tx_time = ms.getMillis();
cad_busy_start = 0;
@@ -159,15 +168,20 @@ 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;
virtual int calcRxDelay(float score, uint32_t air_time) const;
virtual uint32_t getCADFailRetryDelay() const;
virtual uint32_t getCADFailMaxDuration() const;
virtual uint8_t getDefaultTxCodingRate() const { return 0; }
virtual int getInterferenceThreshold() const { return 0; } // disabled by default
virtual int getAGCResetInterval() const { return 0; } // disabled by default
virtual unsigned long getDutyCycleWindowMs() const { return 3600000; }
const Packet* getOutboundInFlight() const { return outbound; }
bool queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis);
public:
void begin();
@@ -175,7 +189,7 @@ public:
Packet* obtainNewPacket();
void releasePacket(Packet* packet);
void sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0);
bool sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0);
unsigned long getTotalAirTime() const { return total_air_time; }
unsigned long getReceiveAirTime() const {return rx_air_time; }
+739 -11
View File
@@ -3,12 +3,127 @@
namespace mesh {
static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 15;
static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15;
static const uint8_t FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT = 3;
static const uint8_t FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX = 15;
static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) {
uint8_t code = flags & 0x03;
uint8_t size_pow2 = (uint8_t)(1U << code); // legacy TRACE interpretation
uint8_t size_linear = (uint8_t)(code + 1U); // packed-size interpretation (1..4)
bool pow2_ok = size_pow2 > 0 && (route_bytes % size_pow2) == 0;
bool linear_ok = size_linear > 0 && (route_bytes % size_linear) == 0;
if (pow2_ok && !linear_ok) {
return size_pow2;
}
if (linear_ok && !pow2_ok) {
return size_linear;
}
if (pow2_ok) {
return size_pow2;
}
return size_linear;
}
void Mesh::begin() {
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
_direct_retries[i].packet = NULL;
_direct_retries[i].trigger_packet = NULL;
_direct_retries[i].retry_started_at = 0;
_direct_retries[i].echo_wait_started_at = 0;
_direct_retries[i].retry_at = 0;
_direct_retries[i].retry_delay = 0;
_direct_retries[i].retry_attempts_sent = 0;
_direct_retries[i].priority = 0;
_direct_retries[i].progress_marker = 0;
_direct_retries[i].expect_path_growth = false;
_direct_retries[i].waiting_final_echo = false;
_direct_retries[i].queued = false;
_direct_retries[i].active = false;
}
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
_flood_retries[i].packet = NULL;
_flood_retries[i].trigger_packet = NULL;
_flood_retries[i].retry_started_at = 0;
_flood_retries[i].retry_at = 0;
_flood_retries[i].retry_delay = 0;
_flood_retries[i].retry_attempts_sent = 0;
_flood_retries[i].priority = 0;
_flood_retries[i].progress_marker = 0;
_flood_retries[i].waiting_final_echo = false;
_flood_retries[i].queued = false;
_flood_retries[i].active = false;
}
Dispatcher::begin();
}
void Mesh::loop() {
Dispatcher::loop();
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
if (!_direct_retries[i].active) {
continue;
}
if (_direct_retries[i].waiting_final_echo) {
if (!millisHasNowPassed(_direct_retries[i].retry_at)) {
continue;
}
uint32_t elapsed_millis = _direct_retries[i].retry_started_at == 0
? 0
: (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at);
onDirectRetryEvent("failed_all_tries", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent);
onDirectRetryEvent("failure", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent);
clearDirectRetrySlot(i);
continue;
}
if (!_direct_retries[i].queued || !millisHasNowPassed(_direct_retries[i].retry_at)) {
continue;
}
if (!isDirectRetryQueued(_direct_retries[i].packet)) {
if (_direct_retries[i].packet == getOutboundInFlight()) {
continue; // currently transmitting; keep slot until onSendComplete/onSendFail emits event
}
clearDirectRetrySlot(i);
}
}
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
if (!_flood_retries[i].active) {
continue;
}
if (_flood_retries[i].waiting_final_echo) {
if (!millisHasNowPassed(_flood_retries[i].retry_at)) {
continue;
}
uint32_t elapsed_millis = _flood_retries[i].retry_started_at == 0
? 0
: (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at);
onFloodRetryEvent("failed_all_tries", _flood_retries[i].packet, elapsed_millis, _flood_retries[i].retry_attempts_sent);
onFloodRetryEvent("failure", _flood_retries[i].packet, elapsed_millis, _flood_retries[i].retry_attempts_sent);
clearFloodRetrySlot(i);
continue;
}
if (!_flood_retries[i].queued || !millisHasNowPassed(_flood_retries[i].retry_at)) {
continue;
}
if (!isFloodRetryQueued(_flood_retries[i].packet)) {
if (_flood_retries[i].packet == getOutboundInFlight()) {
continue;
}
clearFloodRetrySlot(i);
}
}
}
bool Mesh::allowPacketForward(const mesh::Packet* packet) {
@@ -22,10 +137,56 @@ 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) {
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);
}
bool Mesh::allowFloodRetry(const Packet* packet) const {
return true;
}
bool Mesh::hasFloodRetryTargetPrefix(const Packet* packet) const {
return false;
}
uint8_t Mesh::getFloodRetryMaxPathLength(const Packet* packet) const {
return 2;
}
uint8_t Mesh::getFloodRetryMaxAttempts(const Packet* packet) const {
return FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT;
}
uint32_t Mesh::getFloodRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) {
if (packet == NULL) {
return _radio->getEstAirtimeFor(MAX_TRANS_UNIT);
}
uint32_t max_packet_airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT);
uint32_t packet_airtime = _radio->getEstAirtimeFor(packet->getRawLength());
return max_packet_airtime + (20UL * packet_airtime);
}
uint8_t Mesh::getExtraAckTransmitCount() const {
return 0;
}
void Mesh::onSendComplete(Packet* packet) {
armDirectRetryOnSendComplete(packet);
armFloodRetryOnSendComplete(packet);
}
void Mesh::onSendFail(Packet* packet) {
clearPendingDirectRetryOnSendFail(packet);
clearPendingFloodRetryOnSendFail(packet);
}
uint32_t Mesh::getCADFailRetryDelay() const {
return _rng->nextInt(1, 4)*120;
}
@@ -39,6 +200,12 @@ int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int
}
DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
if (pkt->isRouteDirect()) {
cancelDirectRetryOnEcho(pkt);
} else if (pkt->isRouteFlood()) {
cancelFloodRetryOnEcho(pkt);
}
if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) {
if (pkt->path_len < MAX_PATH_SIZE) {
uint8_t i = 0;
@@ -47,17 +214,19 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
uint32_t auth_code;
memcpy(&auth_code, &pkt->payload[i], 4); i += 4;
uint8_t flags = pkt->payload[i++];
uint8_t path_sz = flags & 0x03; // NEW v1.11+: lower 2 bits is path hash size
uint8_t len = pkt->payload_len - i;
uint8_t offset = pkt->path_len << path_sz;
uint8_t hash_size = decodeTraceHashSize(flags, len);
uint16_t offset = (uint16_t)pkt->path_len * (uint16_t)hash_size;
if (offset >= len) { // TRACE has reached end of given path
onTraceRecv(pkt, trace_tag, auth_code, flags, pkt->path, &pkt->payload[i], len);
} else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) {
} else if (hash_size > 0 && offset + hash_size <= len
&& self_id.isHashMatch(&pkt->payload[i + offset], hash_size)
&& allowPacketForward(pkt) && !_tables->hasSeen(pkt)) {
// append SNR (Not hash!)
pkt->path[pkt->path_len++] = (int8_t) (pkt->getSNR()*4);
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 +267,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
}
}
@@ -334,8 +504,10 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
packet->setPathHashCount(n + 1);
uint32_t d = getRetransmitDelay(packet);
uint8_t priority = packet->getPathHashCount();
maybeScheduleFloodRetry(packet, priority);
// as this propagates outwards, give it lower and lower priority
return ACTION_RETRANSMIT_DELAYED(packet->getPathHashCount(), d); // give priority to closer sources, than ones further away
return ACTION_RETRANSMIT_DELAYED(priority, d); // give priority to closer sources, than ones further away
}
return ACTION_RELEASE;
}
@@ -372,6 +544,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 +555,565 @@ 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) {
if (_direct_retries[idx].waiting_final_echo && _direct_retries[idx].packet != NULL) {
releasePacket(_direct_retries[idx].packet);
}
_direct_retries[idx].packet = NULL;
_direct_retries[idx].trigger_packet = NULL;
_direct_retries[idx].retry_started_at = 0;
_direct_retries[idx].echo_wait_started_at = 0;
_direct_retries[idx].retry_at = 0;
_direct_retries[idx].retry_delay = 0;
_direct_retries[idx].retry_attempts_sent = 0;
_direct_retries[idx].priority = 0;
_direct_retries[idx].progress_marker = 0;
_direct_retries[idx].expect_path_growth = false;
_direct_retries[idx].waiting_final_echo = false;
_direct_retries[idx].queued = false;
_direct_retries[idx].active = false;
}
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;
}
int8_t echo_snr_x4 = packet->_snr;
if (_direct_retries[i].queued || _direct_retries[i].waiting_final_echo) {
if (_direct_retries[i].packet != NULL) {
// Success quality comes from the received downstream echo, not the original upstream RX.
_direct_retries[i].packet->_snr = echo_snr_x4;
}
uint32_t echo_millis = _direct_retries[i].echo_wait_started_at == 0
? 0
: (uint32_t)(_ms->getMillis() - _direct_retries[i].echo_wait_started_at);
uint8_t retry_attempt = _direct_retries[i].waiting_final_echo
? _direct_retries[i].retry_attempts_sent
: _direct_retries[i].retry_attempts_sent + 1;
onDirectRetryEvent("good", _direct_retries[i].packet, echo_millis, retry_attempt);
if (_direct_retries[i].queued) {
for (int j = 0; j < _mgr->getOutboundTotal(); j++) {
if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) {
Packet* pending = _mgr->removeOutboundByIdx(j);
if (pending) {
releasePacket(pending);
}
break;
}
}
}
clearDirectRetrySlot(i);
} else {
if (_direct_retries[i].trigger_packet != NULL) {
_direct_retries[i].trigger_packet->_snr = echo_snr_x4;
}
uint32_t echo_millis = _direct_retries[i].echo_wait_started_at == 0
? 0
: (uint32_t)(_ms->getMillis() - _direct_retries[i].echo_wait_started_at);
onDirectRetryEvent("good", _direct_retries[i].trigger_packet, echo_millis, _direct_retries[i].retry_attempts_sent + 1);
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.
uint32_t elapsed_millis = _direct_retries[i].retry_started_at == 0
? 0
: (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at);
onDirectRetryEvent("resent", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1);
_direct_retries[i].echo_wait_started_at = _ms->getMillis();
_direct_retries[i].retry_attempts_sent++;
uint8_t max_attempts = getDirectRetryMaxAttempts(packet);
if (max_attempts < 1) {
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) {
Packet* final_wait = obtainNewPacket();
if (final_wait == NULL) {
onDirectRetryEvent("dropped_no_packet", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent);
onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent);
clearDirectRetrySlot(i);
continue;
}
*final_wait = *packet;
_direct_retries[i].packet = final_wait;
_direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay);
_direct_retries[i].waiting_final_echo = true;
_direct_retries[i].queued = false;
continue;
}
Packet* retry = obtainNewPacket();
if (retry == NULL) {
onDirectRetryEvent("dropped_no_packet", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1);
onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1);
clearDirectRetrySlot(i);
continue;
}
*retry = *packet;
retry->tx_cr = 0;
uint8_t retry_attempt = _direct_retries[i].retry_attempts_sent + 1;
configureDirectRetryPacket(retry, packet, retry_attempt);
uint32_t retry_delay = getDirectRetryAttemptDelay(packet, _direct_retries[i].retry_attempts_sent);
if (queueOutboundPacket(retry, _direct_retries[i].priority, retry_delay)) {
_direct_retries[i].packet = retry;
_direct_retries[i].retry_delay = retry_delay;
_direct_retries[i].retry_at = futureMillis(retry_delay);
_direct_retries[i].waiting_final_echo = false;
onDirectRetryEvent("queued", retry, retry_delay, retry_attempt);
} else {
onDirectRetryEvent("dropped_queue_full", retry, retry_delay, retry_attempt);
onDirectRetryEvent("failure", retry, elapsed_millis, retry_attempt);
releasePacket(retry);
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, 1);
onDirectRetryEvent("failure", packet, 0, 1);
clearDirectRetrySlot(i);
continue;
}
*retry = *packet;
retry->tx_cr = 0;
configureDirectRetryPacket(retry, packet, 1);
// Start the echo wait only after the initial direct transmission actually completed.
if (queueOutboundPacket(retry, _direct_retries[i].priority, _direct_retries[i].retry_delay)) {
unsigned long now = _ms->getMillis();
_direct_retries[i].packet = retry;
_direct_retries[i].trigger_packet = NULL;
_direct_retries[i].queued = true;
_direct_retries[i].waiting_final_echo = false;
_direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay);
_direct_retries[i].retry_started_at = now;
_direct_retries[i].echo_wait_started_at = now;
onDirectRetryEvent("queued", retry, _direct_retries[i].retry_delay, 1);
} else {
onDirectRetryEvent("dropped_queue_full", retry, _direct_retries[i].retry_delay, 1);
onDirectRetryEvent("failure", retry, 0, 1);
releasePacket(retry);
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, _direct_retries[i].retry_attempts_sent + 1);
onDirectRetryEvent("failure", packet, 0, _direct_retries[i].retry_attempts_sent + 1);
clearDirectRetrySlot(i);
}
continue;
}
if (_direct_retries[i].trigger_packet == packet) {
onDirectRetryEvent("dropped_send_fail", packet, 0, 1);
onDirectRetryEvent("failure", packet, 0, 1);
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 route_bytes = packet->payload_len - 9;
uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes);
uint16_t offset = (uint16_t)packet->path_len * (uint16_t)hash_size;
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) {
onDirectRetryEvent("dropped_no_slot", packet, 0, 0);
onDirectRetryEvent("failure", packet, 0, 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_started_at = 0;
_direct_retries[slot_idx].echo_wait_started_at = 0;
_direct_retries[slot_idx].retry_at = 0;
_direct_retries[slot_idx].retry_delay = retry_delay;
_direct_retries[slot_idx].retry_attempts_sent = 0;
_direct_retries[slot_idx].priority = priority;
_direct_retries[slot_idx].progress_marker = progress_marker;
_direct_retries[slot_idx].expect_path_growth = expect_path_growth;
_direct_retries[slot_idx].waiting_final_echo = false;
_direct_retries[slot_idx].queued = false;
_direct_retries[slot_idx].active = true;
}
void Mesh::clearFloodRetrySlot(int idx) {
if (_flood_retries[idx].waiting_final_echo && _flood_retries[idx].packet != NULL) {
releasePacket(_flood_retries[idx].packet);
}
_flood_retries[idx].packet = NULL;
_flood_retries[idx].trigger_packet = NULL;
_flood_retries[idx].retry_started_at = 0;
_flood_retries[idx].retry_at = 0;
_flood_retries[idx].retry_delay = 0;
_flood_retries[idx].retry_attempts_sent = 0;
_flood_retries[idx].priority = 0;
_flood_retries[idx].progress_marker = 0;
_flood_retries[idx].waiting_final_echo = false;
_flood_retries[idx].queued = false;
_flood_retries[idx].active = false;
}
bool Mesh::isFloodRetryQueued(const Packet* packet) const {
for (int i = 0; i < _mgr->getOutboundTotal(); i++) {
if (_mgr->getOutboundByIdx(i) == packet) {
return true;
}
}
return false;
}
bool Mesh::isFloodRetryEchoTarget(const Packet* packet, uint8_t progress_marker) const {
return packet->isRouteFlood() && packet->getPathHashCount() > progress_marker;
}
bool Mesh::cancelFloodRetryOnEcho(const Packet* packet) {
uint8_t recv_key[MAX_HASH_SIZE];
packet->calculatePacketHash(recv_key);
bool cleared = false;
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
if (!_flood_retries[i].active || memcmp(recv_key, _flood_retries[i].retry_key, MAX_HASH_SIZE) != 0) {
continue;
}
if (!isFloodRetryEchoTarget(packet, _flood_retries[i].progress_marker)) {
continue;
}
uint32_t echo_millis = _flood_retries[i].retry_started_at == 0
? 0
: (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at);
uint8_t retry_attempt = _flood_retries[i].waiting_final_echo
? _flood_retries[i].retry_attempts_sent
: _flood_retries[i].retry_attempts_sent + 1;
onFloodRetryEvent("good", packet, echo_millis, retry_attempt);
if (_flood_retries[i].queued) {
for (int j = 0; j < _mgr->getOutboundTotal(); j++) {
if (_mgr->getOutboundByIdx(j) == _flood_retries[i].packet) {
Packet* pending = _mgr->removeOutboundByIdx(j);
if (pending) {
releasePacket(pending);
}
break;
}
}
}
clearFloodRetrySlot(i);
cleared = true;
}
return cleared;
}
void Mesh::armFloodRetryOnSendComplete(const Packet* packet) {
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
if (!_flood_retries[i].active) {
continue;
}
if (_flood_retries[i].queued) {
if (_flood_retries[i].packet != packet) {
continue;
}
uint32_t elapsed_millis = _flood_retries[i].retry_started_at == 0
? 0
: (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at);
onFloodRetryEvent("resent", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1);
_flood_retries[i].retry_attempts_sent++;
uint8_t max_attempts = getFloodRetryMaxAttempts(packet);
if (max_attempts < 1) {
max_attempts = 1;
} else if (max_attempts > FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX) {
max_attempts = FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX;
}
if (_flood_retries[i].retry_attempts_sent >= max_attempts) {
Packet* final_wait = obtainNewPacket();
if (final_wait == NULL) {
onFloodRetryEvent("dropped_no_packet", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent);
onFloodRetryEvent("failure", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent);
clearFloodRetrySlot(i);
continue;
}
*final_wait = *packet;
_flood_retries[i].packet = final_wait;
_flood_retries[i].retry_at = futureMillis(_flood_retries[i].retry_delay);
_flood_retries[i].waiting_final_echo = true;
_flood_retries[i].queued = false;
continue;
}
Packet* retry = obtainNewPacket();
if (retry == NULL) {
onFloodRetryEvent("dropped_no_packet", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1);
onFloodRetryEvent("failure", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1);
clearFloodRetrySlot(i);
continue;
}
*retry = *packet;
uint32_t retry_delay = getFloodRetryAttemptDelay(packet, _flood_retries[i].retry_attempts_sent);
if (queueOutboundPacket(retry, _flood_retries[i].priority, retry_delay)) {
_flood_retries[i].packet = retry;
_flood_retries[i].retry_delay = retry_delay;
_flood_retries[i].retry_at = futureMillis(retry_delay);
_flood_retries[i].retry_started_at = _ms->getMillis();
_flood_retries[i].waiting_final_echo = false;
onFloodRetryEvent("queued", retry, retry_delay, _flood_retries[i].retry_attempts_sent + 1);
} else {
onFloodRetryEvent("dropped_queue_full", retry, retry_delay, _flood_retries[i].retry_attempts_sent + 1);
onFloodRetryEvent("failure", retry, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1);
releasePacket(retry);
clearFloodRetrySlot(i);
}
continue;
}
if (_flood_retries[i].trigger_packet != packet) {
continue;
}
Packet* retry = obtainNewPacket();
if (retry == NULL) {
onFloodRetryEvent("dropped_no_packet", packet, _flood_retries[i].retry_delay, 1);
onFloodRetryEvent("failure", packet, 0, 1);
clearFloodRetrySlot(i);
continue;
}
*retry = *packet;
if (queueOutboundPacket(retry, _flood_retries[i].priority, _flood_retries[i].retry_delay)) {
unsigned long now = _ms->getMillis();
_flood_retries[i].packet = retry;
_flood_retries[i].trigger_packet = NULL;
_flood_retries[i].queued = true;
_flood_retries[i].waiting_final_echo = false;
_flood_retries[i].retry_at = futureMillis(_flood_retries[i].retry_delay);
_flood_retries[i].retry_started_at = now;
onFloodRetryEvent("queued", retry, _flood_retries[i].retry_delay, 1);
} else {
onFloodRetryEvent("dropped_queue_full", retry, _flood_retries[i].retry_delay, 1);
onFloodRetryEvent("failure", retry, 0, 1);
releasePacket(retry);
clearFloodRetrySlot(i);
}
}
}
void Mesh::clearPendingFloodRetryOnSendFail(const Packet* packet) {
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
if (!_flood_retries[i].active) {
continue;
}
if (_flood_retries[i].queued) {
if (_flood_retries[i].packet == packet) {
onFloodRetryEvent("dropped_send_fail", packet, 0, _flood_retries[i].retry_attempts_sent + 1);
onFloodRetryEvent("failure", packet, 0, _flood_retries[i].retry_attempts_sent + 1);
clearFloodRetrySlot(i);
}
continue;
}
if (_flood_retries[i].trigger_packet == packet) {
onFloodRetryEvent("dropped_send_fail", packet, 0, 1);
onFloodRetryEvent("failure", packet, 0, 1);
clearFloodRetrySlot(i);
}
}
}
void Mesh::maybeScheduleFloodRetry(const Packet* packet, uint8_t priority) {
if (packet == NULL || !packet->isRouteFlood() || hasFloodRetryTargetPrefix(packet)) {
return;
}
uint8_t max_path_len = getFloodRetryMaxPathLength(packet);
if (max_path_len != FLOOD_RETRY_PATH_GATE_DISABLED && packet->getPathHashCount() > max_path_len) {
return;
}
uint8_t max_attempts = getFloodRetryMaxAttempts(packet);
if (max_attempts == 0) {
return;
}
int slot_idx = -1;
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
if (!_flood_retries[i].active) {
slot_idx = i;
break;
}
}
if (slot_idx < 0) {
onFloodRetryEvent("dropped_no_slot", packet, 0, 0);
onFloodRetryEvent("failure", packet, 0, 0);
return;
}
if (!allowFloodRetry(packet)) {
return;
}
uint32_t retry_delay = getFloodRetryAttemptDelay(packet, 0);
packet->calculatePacketHash(_flood_retries[slot_idx].retry_key);
_flood_retries[slot_idx].packet = NULL;
_flood_retries[slot_idx].trigger_packet = const_cast<Packet*>(packet);
_flood_retries[slot_idx].retry_started_at = 0;
_flood_retries[slot_idx].retry_at = 0;
_flood_retries[slot_idx].retry_delay = retry_delay;
_flood_retries[slot_idx].retry_attempts_sent = 0;
_flood_retries[slot_idx].priority = priority;
_flood_retries[slot_idx].progress_marker = packet->getPathHashCount();
_flood_retries[slot_idx].waiting_final_echo = false;
_flood_retries[slot_idx].queued = false;
_flood_retries[slot_idx].active = true;
}
Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) {
if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL;
@@ -634,7 +1361,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 +1390,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 +1423,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 +1434,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 +1447,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);
}
}
}
+130
View File
@@ -4,6 +4,18 @@
namespace mesh {
#ifndef MAX_DIRECT_RETRY_SLOTS
#define MAX_DIRECT_RETRY_SLOTS 6
#endif
#ifndef MAX_FLOOD_RETRY_SLOTS
#define MAX_FLOOD_RETRY_SLOTS 6
#endif
#ifndef FLOOD_RETRY_PATH_GATE_DISABLED
#define FLOOD_RETRY_PATH_GATE_DISABLED 0xFF
#endif
class GroupChannel {
public:
uint8_t hash[PATH_HASH_SIZE];
@@ -16,6 +28,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 +37,68 @@ 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_started_at;
unsigned long echo_wait_started_at;
unsigned long retry_at;
uint32_t retry_delay;
uint8_t retry_attempts_sent;
uint8_t retry_key[MAX_HASH_SIZE];
uint8_t priority;
uint8_t progress_marker;
bool expect_path_growth;
bool waiting_final_echo;
bool queued;
bool active;
};
struct FloodRetryEntry {
Packet* packet;
Packet* trigger_packet;
unsigned long retry_started_at;
unsigned long retry_at;
uint32_t retry_delay;
uint8_t retry_attempts_sent;
uint8_t retry_key[MAX_HASH_SIZE];
uint8_t priority;
uint8_t progress_marker;
bool waiting_final_echo;
bool queued;
bool active;
};
RTCClock* _rtc;
RNG* _rng;
MeshTables* _tables;
DirectRetryEntry _direct_retries[MAX_DIRECT_RETRY_SLOTS];
FloodRetryEntry _flood_retries[MAX_FLOOD_RETRY_SLOTS];
void 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 clearFloodRetrySlot(int idx);
bool isFloodRetryQueued(const Packet* packet) const;
bool cancelFloodRetryOnEcho(const Packet* packet);
void armFloodRetryOnSendComplete(const Packet* packet);
void clearPendingFloodRetryOnSendFail(const Packet* packet);
void maybeScheduleFloodRetry(const Packet* packet, uint8_t priority);
//void routeRecvAcks(Packet* packet, uint32_t delay_millis);
DispatcherAction forwardMultipartDirect(Packet* pkt);
protected:
DispatcherAction onRecvPacket(Packet* pkt) override;
void onSendComplete(Packet* packet) override;
void onSendFail(Packet* packet) override;
virtual uint32_t getCADFailRetryDelay() const override;
@@ -65,11 +129,77 @@ 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);
/**
* \brief Decide whether a FLOOD packet should retry when no downstream echo is overheard.
*/
virtual bool allowFloodRetry(const Packet* packet) const;
/**
* \brief Return true when this FLOOD packet already carries an application-defined target prefix.
*/
virtual bool hasFloodRetryTargetPrefix(const Packet* packet) const;
/**
* \returns maximum flood path hash count eligible for retry, or FLOOD_RETRY_PATH_GATE_DISABLED.
*/
virtual uint8_t getFloodRetryMaxPathLength(const Packet* packet) const;
/**
* \returns maximum number of FLOOD retry transmissions after the initial TX.
*/
virtual uint8_t getFloodRetryMaxAttempts(const Packet* packet) const;
/**
* \brief Return true when a received FLOOD echo is enough to cancel a pending retry.
*/
virtual bool isFloodRetryEchoTarget(const Packet* packet, uint8_t progress_marker) const;
/**
* \returns delay before a specific flood retry attempt, where attempt_idx=0 is the first retry.
*/
virtual uint32_t getFloodRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx);
/**
* \brief Optional hook for logging flood-retry lifecycle events.
*/
virtual void onFloodRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { }
/**
* \returns number of extra (Direct) ACK transmissions wanted.
*/
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, uint8_t retry_attempt) { }
/**
* \brief Optional hook to set local-only transmit options on a retry packet before it is queued.
*/
virtual void configureDirectRetryPacket(Packet* retry, const Packet* original, uint8_t retry_attempt) { }
/**
* \brief Perform search of local DB of peers/contacts.
* \returns Number of peers with matching hash
+4
View File
@@ -52,12 +52,16 @@ public:
virtual void onAfterTransmit() { }
virtual void reboot() = 0;
virtual void powerOff() { /* no op */ }
virtual uint32_t getIRQGpio() { return -1; } // not supported. Returns DIO1 (SX1262) and DIO0 (SX127x)
virtual void sleep(uint32_t secs) { /* no op */ }
virtual uint32_t getGpio() { return 0; }
virtual void setGpio(uint32_t values) {}
virtual uint8_t getStartupReason() const = 0;
virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; }
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
virtual bool setLoRaFemLnaEnabled(bool enable) { return false; }
virtual bool canControlLoRaFemLna() const { return false; }
virtual bool isLoRaFemLnaEnabled() const { return false; }
// Power management interface (boards with power management override these)
virtual bool isExternalPowered() { return false; }
+3 -1
View File
@@ -8,6 +8,7 @@ Packet::Packet() {
header = 0;
path_len = 0;
payload_len = 0;
tx_cr = 0;
}
bool Packet::isValidPathLen(uint8_t path_len) {
@@ -64,6 +65,7 @@ uint8_t Packet::writeTo(uint8_t dest[]) const {
bool Packet::readFrom(const uint8_t src[], uint8_t len) {
uint8_t i = 0;
tx_cr = 0;
header = src[i++];
if (hasTransportCodes()) {
memcpy(&transport_codes[0], &src[i], 2); i += 2;
@@ -84,4 +86,4 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) {
return true; // success
}
}
}
+1
View File
@@ -49,6 +49,7 @@ public:
uint8_t path[MAX_PATH_SIZE];
uint8_t payload[MAX_PACKET_PAYLOAD];
int8_t _snr;
uint8_t tx_cr; // volatile local-only TX coding-rate override; not serialized
/**
* \brief calculate the hash of payload + type
+4
View File
@@ -17,6 +17,10 @@ bool ArduinoSerialInterface::isConnected() const {
return true; // no way of knowing, so assume yes
}
bool ArduinoSerialInterface::isReadBusy() const {
return false;
}
bool ArduinoSerialInterface::isWriteBusy() const {
return false;
}
+2 -1
View File
@@ -28,7 +28,8 @@ public:
bool isConnected() const override;
bool isReadBusy() const override;
bool isWriteBusy() const override;
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;
};
};
+1
View File
@@ -15,6 +15,7 @@ public:
virtual bool isConnected() const = 0;
virtual bool isReadBusy() const = 0;
virtual bool isWriteBusy() const = 0;
virtual size_t writeFrame(const uint8_t src[], size_t len) = 0;
virtual size_t checkRecvFrame(uint8_t dest[]) = 0;
+1400 -15
View File
File diff suppressed because it is too large. Load diff
+73 -2
View File
@@ -19,6 +19,57 @@
#define LOOP_DETECT_MODERATE 2
#define LOOP_DETECT_STRICT 3
#define RETRY_PRESET_INFRA 0
#define RETRY_PRESET_ROOFTOP 1
#define RETRY_PRESET_MOBILE 2
#define DIRECT_RETRY_INFRA_BASE_MS 275
#define DIRECT_RETRY_INFRA_COUNT 4
#define DIRECT_RETRY_INFRA_STEP_MS 150
#define DIRECT_RETRY_INFRA_MARGIN_X4 60
#define DIRECT_RETRY_ROOFTOP_BASE_MS 175
#define DIRECT_RETRY_ROOFTOP_COUNT 15
#define DIRECT_RETRY_ROOFTOP_STEP_MS 100
#define DIRECT_RETRY_ROOFTOP_MARGIN_X4 20
#define DIRECT_RETRY_MOBILE_BASE_MS 175
#define DIRECT_RETRY_MOBILE_COUNT 15
#define DIRECT_RETRY_MOBILE_STEP_MS 50
#define DIRECT_RETRY_MOBILE_MARGIN_X4 0
#ifndef FLOOD_RETRY_PREFIX_SLOTS
#define FLOOD_RETRY_PREFIX_SLOTS 8
#endif
#ifndef FLOOD_RETRY_PREFIX_LEN
#define FLOOD_RETRY_PREFIX_LEN 3
#endif
#ifndef FLOOD_RETRY_BRIDGE_BUCKETS
#define FLOOD_RETRY_BRIDGE_BUCKETS 6
#endif
#ifndef FLOOD_RETRY_BUCKET_PREFIXES
#define FLOOD_RETRY_BUCKET_PREFIXES 17
#endif
#ifndef FLOOD_RETRY_IGNORE_PREFIXES
#define FLOOD_RETRY_IGNORE_PREFIXES 8
#endif
#ifndef FLOOD_RETRY_LIST_PREFIXES
#define FLOOD_RETRY_LIST_PREFIXES ((FLOOD_RETRY_IGNORE_PREFIXES > FLOOD_RETRY_BUCKET_PREFIXES) ? FLOOD_RETRY_IGNORE_PREFIXES : FLOOD_RETRY_BUCKET_PREFIXES)
#endif
#ifndef FLOOD_RETRY_LIST_TEXT_MAX
#define FLOOD_RETRY_LIST_TEXT_MAX (FLOOD_RETRY_LIST_PREFIXES * FLOOD_RETRY_PREFIX_LEN * 2 + FLOOD_RETRY_LIST_PREFIXES)
#endif
#ifndef COMMON_CLI_TMP_LEN
#define COMMON_CLI_TMP_LEN ((FLOOD_RETRY_LIST_TEXT_MAX > (PRV_KEY_SIZE * 2 + 4)) ? FLOOD_RETRY_LIST_TEXT_MAX : (PRV_KEY_SIZE * 2 + 4))
#endif
#define DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT 40 // 10.0 dB and up => CR4
#define DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT 30 // 7.5 dB and up => CR5
#define DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT 10 // 2.5 dB and up => CR7
#define DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT 10 // 2.5 dB and down => CR8
#define DIRECT_RETRY_CR_SNR_X4_MIN -128
#define DIRECT_RETRY_CR_SNR_X4_MAX 127
struct NodePrefs { // persisted to file
float airtime_factor;
char node_name[32];
@@ -33,7 +84,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; // stored in quarter-dB units (x4)
uint8_t direct_retry_prefs_magic[2];
uint8_t sf;
uint8_t cr;
uint8_t allow_read_only;
@@ -59,8 +112,26 @@ struct NodePrefs { // persisted to file
float adc_multiplier;
char owner_info[120];
uint8_t rx_boosted_gain; // power settings
uint8_t radio_fem_rxgain; // LoRa FEM RX gain setting
uint8_t path_hash_mode; // which path mode to use when sending
uint8_t loop_detect;
uint8_t direct_retry_attempts;
uint16_t direct_retry_base_ms;
uint8_t direct_retry_timing_magic[2];
uint8_t retry_preset;
uint16_t direct_retry_step_ms;
uint8_t flood_retry_attempts;
uint8_t flood_retry_path_gate;
uint8_t flood_retry_prefs_magic[2];
uint8_t flood_retry_prefixes[FLOOD_RETRY_PREFIX_SLOTS][FLOOD_RETRY_PREFIX_LEN];
uint8_t flood_retry_bridge_enabled;
uint8_t flood_retry_bridge_buckets[FLOOD_RETRY_BRIDGE_BUCKETS][FLOOD_RETRY_BUCKET_PREFIXES][FLOOD_RETRY_PREFIX_LEN];
uint8_t flood_retry_ignore_prefixes[FLOOD_RETRY_IGNORE_PREFIXES][FLOOD_RETRY_PREFIX_LEN];
uint8_t flood_retry_advert_enabled;
int8_t direct_retry_cr4_snr_x4;
int8_t direct_retry_cr5_snr_x4;
int8_t direct_retry_cr7_snr_x4;
int8_t direct_retry_cr8_snr_x4;
};
class CommonCLICallbacks {
@@ -120,7 +191,7 @@ class CommonCLI {
SensorManager* _sensors;
RegionMap* _region_map;
ClientACL* _acl;
char tmp[PRV_KEY_SIZE*2 + 4];
char tmp[COMMON_CLI_TMP_LEN];
mesh::RTCClock* getRTCClock() { return _rtc; }
void savePrefs();
+106 -24
View File
@@ -12,12 +12,14 @@
#include <rom/rtc.h>
#include <sys/time.h>
#include <Wire.h>
#include "driver/rtc_io.h"
#include "soc/rtc.h"
#include "esp_system.h"
class ESP32Board : public mesh::MainBoard {
protected:
uint8_t startup_reason;
bool inhibit_sleep = false;
static inline portMUX_TYPE sleepMux = portMUX_INITIALIZER_UNLOCKED;
public:
void begin() {
@@ -60,25 +62,48 @@ public:
return raw / 4;
}
void enterLightSleep(uint32_t secs) {
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(P_LORA_DIO_1) // Supported ESP32 variants
if (rtc_gpio_is_valid_gpio((gpio_num_t)P_LORA_DIO_1)) { // Only enter sleep mode if P_LORA_DIO_1 is RTC pin
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // To wake up when receiving a LoRa packet
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000); // To wake up every hour to do periodically jobs
}
esp_light_sleep_start(); // CPU enters light sleep
}
#endif
uint32_t getIRQGpio() {
return P_LORA_DIO_1; // default for SX1262
}
void sleep(uint32_t secs) override {
if (!inhibit_sleep) {
enterLightSleep(secs); // To wake up after "secs" seconds or when receiving a LoRa packet
// Skip if not allow to sleep
if (inhibit_sleep) {
delay(1); // Give MCU to OTA to run
return;
}
// Set GPIO wakeup
gpio_num_t wakeupPin = (gpio_num_t)getIRQGpio();
// Configure timer wakeup
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000ULL); // Wake up periodically to do scheduled jobs
}
// Disable CPU interrupt servicing
portENTER_CRITICAL(&sleepMux);
// Skip sleep if there is a LoRa packet
if (gpio_get_level(wakeupPin) == HIGH) {
portEXIT_CRITICAL(&sleepMux);
delay(1);
return;
}
// Configure GPIO wakeup
esp_sleep_enable_gpio_wakeup();
gpio_wakeup_enable((gpio_num_t)wakeupPin, GPIO_INTR_HIGH_LEVEL); // Wake up when receiving a LoRa packet
// MCU enters light sleep
esp_light_sleep_start();
// Avoid ISR flood during wakeup due to HIGH LEVEL interrupt
gpio_wakeup_disable(wakeupPin);
gpio_set_intr_type(wakeupPin, GPIO_INTR_POSEDGE);
// Enable CPU interrupt servicing
portEXIT_CRITICAL(&sleepMux);
}
uint8_t getStartupReason() const override { return startup_reason; }
@@ -102,7 +127,7 @@ public:
#endif
uint16_t getBattMilliVolts() override {
#ifdef PIN_VBAT_READ
#ifdef PIN_VBAT_READ
analogReadResolution(12);
uint32_t raw = 0;
@@ -130,31 +155,88 @@ public:
void setInhibitSleep(bool inhibit) {
inhibit_sleep = inhibit;
}
uint32_t getResetReason() const override {
return esp_reset_reason();
}
// https://docs.espressif.com/projects/esp-idf/en/v4.4.7/esp32/api-reference/system/system.html
const char *getResetReasonString(uint32_t reason) {
switch (reason) {
case ESP_RST_UNKNOWN:
return "Unknown or first boot";
case ESP_RST_POWERON:
return "Power-on reset";
case ESP_RST_EXT:
return "External reset";
case ESP_RST_SW:
return "Software reset";
case ESP_RST_PANIC:
return "Panic / exception reset";
case ESP_RST_INT_WDT:
return "Interrupt watchdog reset";
case ESP_RST_TASK_WDT:
return "Task watchdog reset";
case ESP_RST_WDT:
return "Other watchdog reset";
case ESP_RST_DEEPSLEEP:
return "Wake from deep sleep";
case ESP_RST_BROWNOUT:
return "Brownout (low voltage)";
case ESP_RST_SDIO:
return "SDIO reset";
default:
static char buf[40];
snprintf(buf, sizeof(buf), "Unknown reset reason (%d)", reason);
return buf;
}
}
};
static RTC_NOINIT_ATTR uint32_t _rtc_backup_time;
static RTC_NOINIT_ATTR uint32_t _rtc_backup_magic;
#define RTC_BACKUP_MAGIC 0xAA55CC33
#define RTC_TIME_MIN 1772323200 // 1 Mar 2026
class ESP32RTCClock : public mesh::RTCClock {
public:
ESP32RTCClock() { }
void begin() {
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_POWERON) {
// start with some date/time in the recent past
struct timeval tv;
tv.tv_sec = 1715770351; // 15 May 2024, 8:50pm
tv.tv_usec = 0;
settimeofday(&tv, NULL);
if (reason == ESP_RST_DEEPSLEEP) {
return; // ESP-IDF preserves system time across deep sleep
}
// All other resets (power-on, crash, WDT, brownout) lose system time.
// Restore from RTC backup if valid, otherwise use hardcoded seed.
struct timeval tv;
if (_rtc_backup_magic == RTC_BACKUP_MAGIC && _rtc_backup_time > RTC_TIME_MIN) {
tv.tv_sec = _rtc_backup_time;
} else {
tv.tv_sec = 1772323200; // 1 Mar 2026
}
tv.tv_usec = 0;
settimeofday(&tv, NULL);
}
uint32_t getCurrentTime() override {
time_t _now;
time(&_now);
return _now;
}
void setCurrentTime(uint32_t time) override {
void setCurrentTime(uint32_t time) override {
struct timeval tv;
tv.tv_sec = time;
tv.tv_usec = 0;
settimeofday(&tv, NULL);
_rtc_backup_time = time;
_rtc_backup_magic = RTC_BACKUP_MAGIC;
}
void tick() override {
time_t now;
time(&now);
if (now > RTC_TIME_MIN && (uint32_t)now != _rtc_backup_time) {
_rtc_backup_time = (uint32_t)now;
_rtc_backup_magic = RTC_BACKUP_MAGIC;
}
}
};
+2
View File
@@ -2,6 +2,7 @@
#include <CayenneLPP.h>
#include "sensors/LocationProvider.h"
#include <Wire.h>
#define TELEM_PERM_BASE 0x01 // 'base' permission includes battery
#define TELEM_PERM_LOCATION 0x02
@@ -15,6 +16,7 @@ public:
double node_altitude; // altitude in meters
SensorManager() { node_lat = 0; node_lon = 0; node_altitude = 0; }
virtual bool i2c_probe(TwoWire& wire, uint8_t addr) { return false; }
virtual bool begin() { return false; }
virtual bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { return false; }
virtual void loop() { }
+366 -23
View File
@@ -1,6 +1,9 @@
#pragma once
#include <Mesh.h>
#if ARDUINO
#include <Arduino.h>
#endif
#ifdef ESP32
#include <FS.h>
@@ -8,13 +11,138 @@
#define MAX_PACKET_HASHES 128
#define MAX_PACKET_ACKS 64
#ifndef MAX_RECENT_REPEATERS
// Platform defaults. Can be overridden with -D MAX_RECENT_REPEATERS=<n>.
#if defined(ESP32) || defined(ESP32_PLATFORM)
#define MAX_RECENT_REPEATERS 2048
#elif defined(NRF52_PLATFORM)
#define MAX_RECENT_REPEATERS 512
#else
#define MAX_RECENT_REPEATERS 64
#endif
#endif
#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 {
// Identity and link quality for a next-hop path prefix.
uint8_t prefix[MAX_ROUTE_HASH_BYTES];
uint8_t prefix_len;
int8_t snr_x4;
uint8_t snr_locked;
uint32_t last_heard_millis;
};
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 weightedSnrX4RoundUp(int8_t curr_snr_x4, int8_t new_snr_x4) const {
// Keep existing SNR heavier than a single new sample: 75% existing + 25% new.
int32_t weighted_sum = ((int32_t)curr_snr_x4 * 3) + (int32_t)new_snr_x4;
int32_t blended = weighted_sum / 4; // truncates toward zero
// "Round up" means ceil(), which only differs from truncation for positive remainders.
if (weighted_sum > 0 && (weighted_sum % 4) != 0) {
blended++;
}
if (blended > 127) {
blended = 127;
} else if (blended < -128) {
blended = -128;
}
return (int8_t)blended;
}
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 +151,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,6 +164,11 @@ 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));
// Recent repeater entries are intentionally not restored across boots.
// This avoids struct-layout migration issues and keeps stale path quality
// stats from persisting indefinitely.
memset(_recent_repeaters, 0, sizeof(_recent_repeaters));
_next_recent_repeater_idx = 0;
}
void saveTo(File f) {
f.write(_hashes, sizeof(_hashes));
@@ -42,30 +180,12 @@ public:
bool hasSeen(const mesh::Packet* packet) override {
if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) {
recordRecentRepeater(packet);
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 +193,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 +260,195 @@ 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, bool snr_locked = false,
bool bypass_allow_filter = false) {
if (prefix == NULL || prefix_len == 0) {
return false;
}
if (prefix_len > MAX_ROUTE_HASH_BYTES) {
prefix_len = MAX_ROUTE_HASH_BYTES;
}
if (!bypass_allow_filter && _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;
}
if (snr_locked) {
existing.snr_x4 = snr_x4;
existing.snr_locked = 1;
} else if (!existing.snr_locked) {
existing.snr_x4 = weightedSnrX4RoundUp(existing.snr_x4, snr_x4);
}
#if ARDUINO
existing.last_heard_millis = millis();
#else
existing.last_heard_millis = 0;
#endif
return true;
}
int slot_idx = -1;
// Prefer empty slots first while preserving newest-order iteration.
for (int i = 0; i < MAX_RECENT_REPEATERS; i++) {
int idx = (_next_recent_repeater_idx + i) % MAX_RECENT_REPEATERS;
if (_recent_repeaters[idx].prefix_len == 0) {
slot_idx = idx;
break;
}
}
if (slot_idx < 0) {
// Table is full: evict the weakest observed SNR entry.
slot_idx = 0;
int8_t min_snr_x4 = _recent_repeaters[0].snr_x4;
for (int i = 1; i < MAX_RECENT_REPEATERS; i++) {
if (_recent_repeaters[i].snr_x4 < min_snr_x4) {
min_snr_x4 = _recent_repeaters[i].snr_x4;
slot_idx = i;
}
}
}
RecentRepeaterInfo& slot = _recent_repeaters[slot_idx];
memset(slot.prefix, 0, sizeof(slot.prefix));
memcpy(slot.prefix, prefix, prefix_len);
slot.prefix_len = prefix_len;
slot.snr_x4 = snr_x4;
slot.snr_locked = snr_locked ? 1 : 0;
#if ARDUINO
slot.last_heard_millis = millis();
#else
slot.last_heard_millis = 0;
#endif
_next_recent_repeater_idx = (slot_idx + 1) % MAX_RECENT_REPEATERS;
return true;
}
bool decrementRecentRepeaterSnrX4(const uint8_t* prefix, uint8_t prefix_len, uint8_t amount_x4 = 1) {
if (prefix == NULL || prefix_len == 0 || amount_x4 == 0) {
return false;
}
if (prefix_len > MAX_ROUTE_HASH_BYTES) {
prefix_len = MAX_ROUTE_HASH_BYTES;
}
for (int i = 0; i < MAX_RECENT_REPEATERS; i++) {
RecentRepeaterInfo& existing = _recent_repeaters[i];
if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) {
continue;
}
if (prefix_len > existing.prefix_len) {
memset(existing.prefix, 0, sizeof(existing.prefix));
memcpy(existing.prefix, prefix, prefix_len);
existing.prefix_len = prefix_len;
}
if (!existing.snr_locked) {
int16_t lowered = (int16_t)existing.snr_x4 - (int16_t)amount_x4;
if (lowered < -128) {
lowered = -128;
}
existing.snr_x4 = (int8_t)lowered;
}
return true;
}
return false;
}
const RecentRepeaterInfo* getLatestRecentRepeater() const {
for (int i = 0; i < MAX_RECENT_REPEATERS; i++) {
int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS;
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 clearRecentRepeaters() {
memset(_recent_repeaters, 0, sizeof(_recent_repeaters));
_next_recent_repeater_idx = 0;
}
void resetStats() { _direct_dups = _flood_dups = 0; }
};
+3 -2
View File
@@ -83,11 +83,12 @@ void StaticPoolPacketManager::free(mesh::Packet* packet) {
unused.add(packet, 0, 0);
}
void StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) {
bool StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) {
if (!send_queue.add(packet, priority, scheduled_for)) {
MESH_DEBUG_PRINTLN("queueOutbound: send queue full, dropping packet");
free(packet);
return false;
}
return true;
}
mesh::Packet* StaticPoolPacketManager::getNextOutbound(uint32_t now) {
+2 -2
View File
@@ -26,7 +26,7 @@ public:
mesh::Packet* allocNew() override;
void free(mesh::Packet* packet) override;
void queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) override;
bool queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) override;
mesh::Packet* getNextOutbound(uint32_t now) override;
int getOutboundCount(uint32_t now) const override;
int getOutboundTotal() const override;
@@ -35,4 +35,4 @@ public:
mesh::Packet* removeOutboundByIdx(int i) override;
void queueInbound(mesh::Packet* packet, uint32_t scheduled_for) override;
mesh::Packet* getNextInbound(uint32_t now) override;
};
};
+4
View File
@@ -182,6 +182,10 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
#define BLE_WRITE_MIN_INTERVAL 60
bool SerialBLEInterface::isReadBusy() const {
return (recv_queue_len > 0);
}
bool SerialBLEInterface::isWriteBusy() const {
return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write?
}
+1
View File
@@ -76,6 +76,7 @@ public:
bool isConnected() const override;
bool isReadBusy() const override;
bool isWriteBusy() const override;
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;
+7 -7
View File
@@ -59,13 +59,13 @@
// uint32_t P_LORA_BUSY = 0; //shared, so define at run
// uint32_t P_LORA_DIO_2 = 0; //SX1276 only, so define at run
#define P_LORA_DIO_0 26
#define P_LORA_DIO_1 33
#define P_LORA_NSS 18
#define P_LORA_RESET 23
#define P_LORA_SCLK 5
#define P_LORA_MISO 19
#define P_LORA_MOSI 27
// #define P_LORA_DIO_0 26
// #define P_LORA_DIO_1 33
// #define P_LORA_NSS 18
// #define P_LORA_RESET 23
// #define P_LORA_SCLK 5
// #define P_LORA_MISO 19
// #define P_LORA_MOSI 27
// #define PIN_GPS_RX 34
// #define PIN_GPS_TX 12
+4
View File
@@ -401,6 +401,10 @@ bool SerialBLEInterface::isConnected() const {
return _isDeviceConnected && Bluefruit.connected() > 0;
}
bool SerialBLEInterface::isReadBusy() const {
return (recv_queue_len > 0);
}
bool SerialBLEInterface::isWriteBusy() const {
return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3);
}
+1
View File
@@ -66,6 +66,7 @@ public:
void disable() override;
bool isEnabled() const override { return _isEnabled; }
bool isConnected() const override;
bool isReadBusy() const override;
bool isWriteBusy() const override;
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;
@@ -20,6 +20,15 @@ public:
int sf = ((CustomLLCC68 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len);
}
bool setCodingRate(uint8_t cr) override {
idle();
int err = ((CustomLLCC68 *)_radio)->setCodingRate(cr);
if (err != RADIOLIB_ERR_NONE) {
MESH_DEBUG_PRINTLN("CustomLLCC68Wrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err);
return false;
}
return true;
}
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
@@ -25,6 +25,16 @@ public:
float getLastRSSI() const override { return ((CustomLR1110 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomLR1110 *)_radio)->getSNR(); }
bool setCodingRate(uint8_t cr) override {
idle();
int err = ((CustomLR1110 *)_radio)->setCodingRate(cr);
if (err != RADIOLIB_ERR_NONE) {
MESH_DEBUG_PRINTLN("CustomLR1110Wrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err);
return false;
}
return true;
}
void setRxBoostedGainMode(bool en) override {
((CustomLR1110 *)_radio)->setRxBoostedGainMode(en);
}
@@ -21,6 +21,15 @@ public:
int sf = ((CustomSTM32WLx *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len);
}
bool setCodingRate(uint8_t cr) override {
idle();
int err = ((CustomSTM32WLx *)_radio)->setCodingRate(cr);
if (err != RADIOLIB_ERR_NONE) {
MESH_DEBUG_PRINTLN("CustomSTM32WLxWrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err);
return false;
}
return true;
}
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
};
@@ -24,6 +24,15 @@ public:
int sf = ((CustomSX1262 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len);
}
bool setCodingRate(uint8_t cr) override {
idle();
int err = ((CustomSX1262 *)_radio)->setCodingRate(cr);
if (err != RADIOLIB_ERR_NONE) {
MESH_DEBUG_PRINTLN("CustomSX1262Wrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err);
return false;
}
return true;
}
virtual void powerOff() override {
((CustomSX1262 *)_radio)->sleep(false);
}
@@ -24,6 +24,15 @@ public:
int sf = ((CustomSX1268 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len);
}
bool setCodingRate(uint8_t cr) override {
idle();
int err = ((CustomSX1268 *)_radio)->setCodingRate(cr);
if (err != RADIOLIB_ERR_NONE) {
MESH_DEBUG_PRINTLN("CustomSX1268Wrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err);
return false;
}
return true;
}
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
@@ -23,4 +23,13 @@ public:
int sf = ((CustomSX1276 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len);
}
bool setCodingRate(uint8_t cr) override {
idle();
int err = ((CustomSX1276 *)_radio)->setCodingRate(cr);
if (err != RADIOLIB_ERR_NONE) {
MESH_DEBUG_PRINTLN("CustomSX1276Wrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err);
return false;
}
return true;
}
};
@@ -7,8 +7,10 @@
#endif
#ifdef ENV_INCLUDE_BME680
#ifndef TELEM_BME680_ADDRESS
#define TELEM_BME680_ADDRESS 0x76
#if defined(TELEM_BME680_ADDRESS)
uint8_t TELEM_BME680_ADDRESSES[] = { TELEM_BME680_ADDRESS };
#else
uint8_t TELEM_BME680_ADDRESSES[] = { 0x76, 0x77 }; // Known I2C addresses for BME680
#endif
#define TELEM_BME680_SEALEVELPRESSURE_HPA (1013.25)
#include <Adafruit_BME680.h>
@@ -28,8 +30,10 @@ static Adafruit_AHTX0 AHTX0;
#endif
#if ENV_INCLUDE_BME280
#ifndef TELEM_BME280_ADDRESS
#define TELEM_BME280_ADDRESS 0x76 // BME280 environmental sensor I2C address
#if defined(TELEM_BME280_ADDRESS)
uint8_t TELEM_BME280_ADDRESSES[] = { TELEM_BME280_ADDRESS };
#else
uint8_t TELEM_BME280_ADDRESSES[] = { 0x76, 0x77 }; // Known I2C addresses for BME280
#endif
#define TELEM_BME280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level
#include <Adafruit_BME280.h>
@@ -37,8 +41,10 @@ static Adafruit_BME280 BME280;
#endif
#if ENV_INCLUDE_BMP280
#ifndef TELEM_BMP280_ADDRESS
#define TELEM_BMP280_ADDRESS 0x76 // BMP280 environmental sensor I2C address
#if defined(TELEM_BMP280_ADDRESS)
uint8_t TELEM_BMP280_ADDRESSES[] = { TELEM_BMP280_ADDRESS };
#else
uint8_t TELEM_BMP280_ADDRESSES[] = { 0x76, 0x77 }; // Known I2C addresses for BMP280
#endif
#define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level
#include <Adafruit_BMP280.h>
@@ -161,6 +167,12 @@ public:
static RAK12500LocationProvider RAK12500_provider;
#endif
bool EnvironmentSensorManager::i2c_probe(TwoWire &wire, uint8_t addr) {
wire.beginTransmission(addr);
uint8_t error = wire.endTransmission();
return (error == 0); // If found i2c device, the error is 0
}
bool EnvironmentSensorManager::begin() {
#if ENV_INCLUDE_GPS
#ifdef RAK_WISBLOCK_GPS
@@ -182,7 +194,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_AHTX0
if (AHTX0.begin(TELEM_WIRE, 0, TELEM_AHTX_ADDRESS)) {
if (i2c_probe(*TELEM_WIRE, TELEM_AHTX_ADDRESS) && AHTX0.begin(TELEM_WIRE, 0, TELEM_AHTX_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found AHT10/AHT20 at address: %02X", TELEM_AHTX_ADDRESS);
AHTX0_initialized = true;
} else {
@@ -192,46 +204,55 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_BME680
if (BME680.begin(TELEM_BME680_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found BME680 at address: %02X", TELEM_BME680_ADDRESS);
BME680_initialized = true;
} else {
BME680_initialized = false;
MESH_DEBUG_PRINTLN("BME680 was not found at I2C address %02X", TELEM_BME680_ADDRESS);
for (size_t i = 0; i < sizeof(TELEM_BME680_ADDRESSES) / sizeof(TELEM_BME680_ADDRESSES[0]); i++) {
if (i2c_probe(*TELEM_WIRE, TELEM_BME680_ADDRESSES[i]) && BME680.begin(TELEM_BME680_ADDRESSES[i], TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found BME680 at address: %02X", TELEM_BME680_ADDRESSES[i]);
BME680_initialized = true;
break;
} else {
BME680_initialized = false;
MESH_DEBUG_PRINTLN("BME680 was not found at I2C address %02X", TELEM_BME680_ADDRESSES[i]);
}
}
#endif
#if ENV_INCLUDE_BME280
if (BME280.begin(TELEM_BME280_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found BME280 at address: %02X", TELEM_BME280_ADDRESS);
MESH_DEBUG_PRINTLN("BME sensor ID: %02X", BME280.sensorID());
// Reduce self-heating: single-shot conversions, light oversampling, long standby.
BME280.setSampling(Adafruit_BME280::MODE_FORCED,
for (size_t i = 0; i < sizeof(TELEM_BME280_ADDRESSES) / sizeof(TELEM_BME280_ADDRESSES[0]); i++) {
if (i2c_probe(*TELEM_WIRE, TELEM_BME280_ADDRESSES[i]) && BME280.begin(TELEM_BME280_ADDRESSES[i], TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found BME280 at address: %02X", TELEM_BME280_ADDRESSES[i]);
MESH_DEBUG_PRINTLN("BME sensor ID: %02X", BME280.sensorID());
// Reduce self-heating: single-shot conversions, light oversampling, long standby.
BME280.setSampling(Adafruit_BME280::MODE_FORCED,
Adafruit_BME280::SAMPLING_X1, // temperature
Adafruit_BME280::SAMPLING_X1, // pressure
Adafruit_BME280::SAMPLING_X1, // humidity
Adafruit_BME280::FILTER_OFF,
Adafruit_BME280::STANDBY_MS_1000);
BME280_initialized = true;
} else {
BME280_initialized = false;
MESH_DEBUG_PRINTLN("BME280 was not found at I2C address %02X", TELEM_BME280_ADDRESS);
BME280_initialized = true;
break;
} else {
BME280_initialized = false;
MESH_DEBUG_PRINTLN("BME280 was not found at I2C address %02X", TELEM_BME280_ADDRESSES[i]);
}
}
#endif
#endif
#if ENV_INCLUDE_BMP280
if (BMP280.begin(TELEM_BMP280_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found BMP280 at address: %02X", TELEM_BMP280_ADDRESS);
MESH_DEBUG_PRINTLN("BMP sensor ID: %02X", BMP280.sensorID());
BMP280_initialized = true;
} else {
BMP280_initialized = false;
MESH_DEBUG_PRINTLN("BMP280 was not found at I2C address %02X", TELEM_BMP280_ADDRESS);
}
#endif
for (size_t i = 0; i < sizeof(TELEM_BMP280_ADDRESSES) / sizeof(TELEM_BMP280_ADDRESSES[0]); i++) {
if (i2c_probe(*TELEM_WIRE, TELEM_BMP280_ADDRESSES[i]) && BMP280.begin(TELEM_BMP280_ADDRESSES[i])) {
MESH_DEBUG_PRINTLN("Found BMP280 at address: %02X", TELEM_BMP280_ADDRESSES[i]);
MESH_DEBUG_PRINTLN("BMP sensor ID: %02X", BMP280.sensorID());
BMP280_initialized = true;
break;
} else {
BMP280_initialized = false;
MESH_DEBUG_PRINTLN("BMP280 was not found at I2C address %02X", TELEM_BMP280_ADDRESSES[i]);
}
}
#endif
#if ENV_INCLUDE_SHTC3
if (SHTC3.begin(TELEM_WIRE)) {
if (i2c_probe(*TELEM_WIRE, 0x70) && SHTC3.begin(TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found sensor: SHTC3");
SHTC3_initialized = true;
} else {
@@ -242,21 +263,23 @@ bool EnvironmentSensorManager::begin() {
#if ENV_INCLUDE_SHT4X
SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS);
uint32_t serialNumber = 0;
int16_t sht4x_error;
sht4x_error = SHT4X.serialNumber(serialNumber);
if (sht4x_error == 0) {
MESH_DEBUG_PRINTLN("Found SHT4X at address: %02X", TELEM_SHT4X_ADDRESS);
SHT4X_initialized = true;
} else {
SHT4X_initialized = false;
MESH_DEBUG_PRINTLN("SHT4X was not found at I2C address %02X", TELEM_SHT4X_ADDRESS);
if (i2c_probe(*TELEM_WIRE, TELEM_SHT4X_ADDRESS)) {
SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS);
uint32_t serialNumber = 0;
int16_t sht4x_error;
sht4x_error = SHT4X.serialNumber(serialNumber);
if (sht4x_error == 0) {
MESH_DEBUG_PRINTLN("Found SHT4X at address: %02X", TELEM_SHT4X_ADDRESS);
SHT4X_initialized = true;
} else {
SHT4X_initialized = false;
MESH_DEBUG_PRINTLN("SHT4X was not found at I2C address %02X", TELEM_SHT4X_ADDRESS);
}
}
#endif
#if ENV_INCLUDE_LPS22HB
if (LPS22HB.begin()) {
if (i2c_probe(*TELEM_WIRE, 0x5C) && LPS22HB.begin()) {
MESH_DEBUG_PRINTLN("Found sensor: LPS22HB");
LPS22HB_initialized = true;
} else {
@@ -266,7 +289,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_INA3221
if (INA3221.begin(TELEM_INA3221_ADDRESS, TELEM_WIRE)) {
if (i2c_probe(*TELEM_WIRE, TELEM_INA3221_ADDRESS) && INA3221.begin(TELEM_INA3221_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS);
MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID());
@@ -281,7 +304,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_INA219
if (INA219.begin(TELEM_WIRE)) {
if (i2c_probe(*TELEM_WIRE, TELEM_INA219_ADDRESS) && INA219.begin(TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS);
INA219_initialized = true;
} else {
@@ -291,17 +314,17 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_INA260
if (INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) {
if (i2c_probe(*TELEM_WIRE, TELEM_INA260_ADDRESS) && INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found INA260 at address: %02X", TELEM_INA260_ADDRESS);
INA260_initialized = true;
} else {
INA260_initialized = false;
MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA260_ADDRESS);
MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA219_ADDRESS);
}
#endif
#if ENV_INCLUDE_INA226
if (INA226.begin()) {
if (i2c_probe(*TELEM_WIRE, TELEM_INA226_ADDRESS) && INA226.begin()) {
MESH_DEBUG_PRINTLN("Found INA226 at address: %02X", TELEM_INA226_ADDRESS);
INA226.setMaxCurrentShunt(TELEM_INA226_MAX_AMP, TELEM_INA226_SHUNT_VALUE);
INA226_initialized = true;
@@ -312,7 +335,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_MLX90614
if (MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) {
if (i2c_probe(*TELEM_WIRE, TELEM_MLX90614_ADDRESS) && MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found MLX90614 at address: %02X", TELEM_MLX90614_ADDRESS);
MLX90614_initialized = true;
} else {
@@ -322,7 +345,7 @@ bool EnvironmentSensorManager::begin() {
#endif
#if ENV_INCLUDE_VL53L0X
if (VL53L0X.begin(TELEM_VL53L0X_ADDRESS, false, TELEM_WIRE)) {
if (i2c_probe(*TELEM_WIRE, TELEM_VL53L0X_ADDRESS) && VL53L0X.begin(TELEM_VL53L0X_ADDRESS, false, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found VL53L0X at address: %02X", TELEM_VL53L0X_ADDRESS);
VL53L0X_initialized = true;
} else {
@@ -334,7 +357,7 @@ bool EnvironmentSensorManager::begin() {
#if ENV_INCLUDE_BMP085
// First argument is MODE (aka oversampling)
// choose ULTRALOWPOWER
if (BMP085.begin(0, TELEM_WIRE)) {
if (i2c_probe(*TELEM_WIRE, 0x77) && BMP085.begin(0, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found sensor BMP085");
BMP085_initialized = true;
} else {
@@ -345,7 +368,7 @@ bool EnvironmentSensorManager::begin() {
#if ENV_INCLUDE_RAK12035
RAK12035.setup(*TELEM_WIRE);
if (RAK12035.begin(TELEM_RAK12035_ADDRESS)) {
if (i2c_probe(*TELEM_WIRE, TELEM_RAK12035_ADDRESS) && RAK12035.begin(TELEM_RAK12035_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found sensor RAK12035 at address: %02X", TELEM_RAK12035_ADDRESS);
RAK12035_initialized = true;
} else {
@@ -716,7 +739,7 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
gps_detected = true;
return true;
}
pinMode(ioPin, INPUT);
MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next");
return false;
@@ -759,7 +782,7 @@ void EnvironmentSensorManager::loop() {
#if ENV_INCLUDE_GPS
if (gps_active) {
_location->loop();
_location->loop();
}
if (millis() > next_gps_update) {
@@ -47,6 +47,7 @@ public:
#else
EnvironmentSensorManager(){};
#endif
bool i2c_probe(TwoWire& wire, uint8_t addr) override;
bool begin() override;
bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override;
#if ENV_INCLUDE_GPS
+1 -1
View File
@@ -7,7 +7,7 @@ build_flags = ${nrf52_base.build_flags}
-I variants/gat562_30s_mesh_kit
-D RAK_4631
-D RAK_BOARD
-D NRF52_POWER_MANAGEMENT
; -D NRF52_POWER_MANAGEMENT
-D PIN_BOARD_SCL=14
-D PIN_BOARD_SDA=13
-D PIN_OLED_RESET=-1
+1 -1
View File
@@ -5,7 +5,7 @@ board_check = true
build_flags = ${nrf52_base.build_flags}
${sensor_base.build_flags}
-I variants/gat562_mesh_evb_pro
-D NRF52_POWER_MANAGEMENT
; -D NRF52_POWER_MANAGEMENT
-D PIN_BOARD_SCL=14
-D PIN_BOARD_SDA=13
-D RADIO_CLASS=CustomSX1262
@@ -5,7 +5,7 @@ board_check = true
build_flags = ${nrf52_base.build_flags}
${sensor_base.build_flags}
-I variants/gat562_mesh_tracker_pro
-D NRF52_POWER_MANAGEMENT
; -D NRF52_POWER_MANAGEMENT
-D PIN_BOARD_SCL=14
-D PIN_BOARD_SDA=13
-D PIN_OLED_RESET=-1
+1 -1
View File
@@ -8,7 +8,7 @@ build_flags = ${nrf52_base.build_flags}
-I variants/gat562_mesh_watch13
-D RAK_4631
-D RAK_BOARD
-D NRF52_POWER_MANAGEMENT
; -D NRF52_POWER_MANAGEMENT
-D PIN_BOARD_SCL=14
-D PIN_BOARD_SDA=13
-D PIN_OLED_RESET=-1
+3 -2
View File
@@ -12,10 +12,11 @@ class LoRaFEMControl
void setRxModeEnable(void);
void setRxModeEnableWhenMCUSleep(void);
void setLNAEnable(bool enabled);
bool isLnaCanControl(void) { return lna_can_control; }
bool isLnaCanControl(void) const { return lna_can_control; }
void setLnaCanControl(bool can_control) { lna_can_control = can_control; }
bool isLNAEnabled(void) const { return lna_enabled; }
private:
bool lna_enabled = false;
bool lna_enabled = true;
bool lna_can_control = false;
};
+19 -1
View File
@@ -123,4 +123,22 @@ void T096Board::powerOff() {
const char* T096Board::getManufacturerName() const {
return "Heltec T096";
}
}
bool T096Board::setLoRaFemLnaEnabled(bool enable) {
if (!loRaFEMControl.isLnaCanControl()) {
return false;
}
loRaFEMControl.setLNAEnable(enable);
loRaFEMControl.setRxModeEnable();
return true;
}
bool T096Board::canControlLoRaFemLna() const {
return loRaFEMControl.isLnaCanControl();
}
bool T096Board::isLoRaFemLnaEnabled() const {
return loRaFEMControl.isLNAEnabled();
}
+3
View File
@@ -25,4 +25,7 @@ public:
uint16_t getBattMilliVolts() override;
const char* getManufacturerName() const override ;
void powerOff() override;
bool setLoRaFemLnaEnabled(bool enable) override;
bool canControlLoRaFemLna() const override;
bool isLoRaFemLnaEnabled() const override;
};
+25 -1
View File
@@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags}
-I variants/heltec_t096
-I src/helpers/ui
-D HELTEC_T096
-D NRF52_POWER_MANAGEMENT
; -D NRF52_POWER_MANAGEMENT
-D P_LORA_DIO_1=21
-D P_LORA_NSS=5
-D P_LORA_RESET=16
@@ -126,6 +126,30 @@ lib_deps =
${Heltec_t096.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_t096_companion_radio_ble_femoff]
extends = Heltec_t096
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_flags =
${Heltec_t096.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D ENV_INCLUDE_GPS=1 ; enable the GPS page in UI
; -D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
-D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off
build_src_filter = ${Heltec_t096.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_t096.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_t096_companion_radio_usb]
extends = Heltec_t096
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
+1 -1
View File
@@ -12,7 +12,7 @@ build_flags = ${nrf52_base.build_flags}
-I variants/heltec_t114
-I src/helpers/ui
-D HELTEC_T114
-D NRF52_POWER_MANAGEMENT
; -D NRF52_POWER_MANAGEMENT
-D P_LORA_DIO_1=20
-D P_LORA_NSS=24
-D P_LORA_RESET=25
+27
View File
@@ -99,6 +99,33 @@ lib_deps =
${Heltec_tracker_base.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_Wireless_Tracker_companion_radio_ble_ps]
extends = Heltec_tracker_base
platform_packages =
framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17
build_flags =
${Heltec_tracker_base.build_flags}
-I src/helpers/ui
-I examples/companion_radio/ui-new
-D DISPLAY_ROTATION=1
-D DISPLAY_CLASS=ST7735Display
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456 ; HWT will use display for pin
-D OFFLINE_QUEUE_SIZE=256
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_tracker_base.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
+<helpers/ui/ST7735Display.cpp>
lib_deps =
${Heltec_tracker_base.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_Wireless_Tracker_repeater]
extends = Heltec_tracker_base
build_flags =
@@ -82,3 +82,21 @@ void HeltecTrackerV2Board::begin() {
const char* HeltecTrackerV2Board::getManufacturerName() const {
return "Heltec Tracker V2";
}
bool HeltecTrackerV2Board::setLoRaFemLnaEnabled(bool enable) {
if (!loRaFEMControl.isLnaCanControl()) {
return false;
}
loRaFEMControl.setLNAEnable(enable);
loRaFEMControl.setRxModeEnable();
return true;
}
bool HeltecTrackerV2Board::canControlLoRaFemLna() const {
return loRaFEMControl.isLnaCanControl();
}
bool HeltecTrackerV2Board::isLoRaFemLnaEnabled() const {
return loRaFEMControl.isLNAEnabled();
}
@@ -21,5 +21,8 @@ public:
void powerOff() override;
uint16_t getBattMilliVolts() override;
const char* getManufacturerName() const override ;
bool setLoRaFemLnaEnabled(bool enable) override;
bool canControlLoRaFemLna() const override;
bool isLoRaFemLnaEnabled() const override;
};
+3 -2
View File
@@ -12,10 +12,11 @@ class LoRaFEMControl
void setRxModeEnable(void);
void setRxModeEnableWhenMCUSleep(void);
void setLNAEnable(bool enabled);
bool isLnaCanControl(void) { return lna_can_control; }
bool isLnaCanControl(void) const { return lna_can_control; }
void setLnaCanControl(bool can_control) { lna_can_control = can_control; }
bool isLNAEnabled(void) const { return lna_enabled; }
private:
bool lna_enabled = false;
bool lna_enabled = true;
bool lna_can_control = false;
};
+27
View File
@@ -176,6 +176,33 @@ lib_deps =
${Heltec_tracker_v2.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_tracker_v2_companion_radio_ble_ps]
extends = Heltec_tracker_v2
platform_packages =
framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17
build_flags =
${Heltec_tracker_v2.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=ST7735Display
-D BLE_PIN_CODE=123456 ; dynamic, random PIN
-D AUTO_SHUTDOWN_MILLIVOLTS=3400
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_tracker_v2.build_src_filter}
+<helpers/ui/ST7735Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_tracker_v2.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_tracker_v2_companion_radio_wifi]
extends = Heltec_tracker_v2
build_flags =
+10 -6
View File
@@ -17,12 +17,12 @@ public:
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep)
if (wakeup_source & (1 << P_LORA_DIO_0)) { // received a LoRa packet (while in deep sleep)
startup_reason = BD_STARTUP_RX_PACKET;
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_0);
}
}
@@ -30,15 +30,15 @@ public:
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
// Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_0, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_0);
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
if (pin_wake_btn < 0) {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_0), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet
} else {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_0) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn
}
if (secs > 0) {
@@ -64,4 +64,8 @@ public:
const char* getManufacturerName() const override {
return "Heltec V2";
}
uint32_t getIRQGpio() override {
return P_LORA_DIO_0; // default for SX1276
}
};
+28 -3
View File
@@ -7,10 +7,10 @@ build_flags =
-D HELTEC_LORA_V2
-D RADIO_CLASS=CustomSX1276
-D WRAPPER_CLASS=CustomSX1276Wrapper
-D P_LORA_DIO_1=26
-D P_LORA_DIO_0=26
-D P_LORA_DIO_1=35
-D P_LORA_NSS=18
-D P_LORA_RESET=RADIOLIB_NC
-D P_LORA_BUSY=RADIOLIB_NC
-D P_LORA_RESET=14
-D P_LORA_SCLK=5
-D P_LORA_MISO=19
-D P_LORA_MOSI=27
@@ -172,6 +172,31 @@ lib_deps =
${Heltec_lora32_v2.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_v2_companion_radio_ble_ps]
extends = Heltec_lora32_v2
platform_packages =
framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17
build_flags =
${Heltec_lora32_v2.build_flags}
-I examples/companion_radio/ui-new
-D DISPLAY_CLASS=SSD1306Display
-D MAX_CONTACTS=160
-D MAX_GROUP_CHANNELS=8
-D BLE_PIN_CODE=123456
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v2.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_lora32_v2.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_v2_companion_radio_wifi]
extends = Heltec_lora32_v2
build_flags =
+2 -2
View File
@@ -5,9 +5,9 @@ HeltecV2Board board;
#if defined(P_LORA_SCLK)
static SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi);
#else
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1);
#endif
WRAPPER_CLASS radio_driver(radio, board);
+4
View File
@@ -53,6 +53,10 @@ public:
}
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) {
// Clear stale wakeup sources to avoid ghost wakeup
// This is required when Power Management and automatic lightsleep are enabled
esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
// Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep
+46
View File
@@ -179,6 +179,32 @@ lib_deps =
${Heltec_lora32_v3.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_v3_companion_radio_ble_ps]
extends = Heltec_lora32_v3
platform_packages =
framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17
build_flags =
${Heltec_lora32_v3.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=SSD1306Display
-D BLE_PIN_CODE=123456 ; dynamic, random PIN
-D AUTO_SHUTDOWN_MILLIVOLTS=3400
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_lora32_v3.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_v3_companion_radio_wifi]
extends = Heltec_lora32_v3
build_flags =
@@ -320,6 +346,26 @@ lib_deps =
${Heltec_lora32_v3.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_WSL3_companion_radio_ble_ps]
extends = Heltec_lora32_v3
platform_packages =
framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17
build_flags =
${Heltec_lora32_v3.build_flags}
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
lib_deps =
${Heltec_lora32_v3.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_WSL3_companion_radio_usb]
extends = Heltec_lora32_v3
build_flags =
+26
View File
@@ -33,6 +33,10 @@ void HeltecV4Board::begin() {
}
void HeltecV4Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) {
// Clear stale wakeup sources to avoid ghost wakeup
// This is required when Power Management and automatic lightsleep are enabled
esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
// Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep
@@ -83,3 +87,25 @@ void HeltecV4Board::begin() {
return loRaFEMControl.getFEMType() == KCT8103L_PA ? "Heltec V4.3 OLED" : "Heltec V4 OLED";
#endif
}
bool HeltecV4Board::setLoRaFemLnaEnabled(bool enable) {
#if defined(RADIO_FEM_RXGAIN) && (RADIO_FEM_RXGAIN == 0)
enable = false;
#endif
if (!loRaFEMControl.isLnaCanControl()) {
return false;
}
loRaFEMControl.setLNAEnable(enable);
loRaFEMControl.setRxModeEnable();
return true;
}
bool HeltecV4Board::canControlLoRaFemLna() const {
return loRaFEMControl.isLnaCanControl();
}
bool HeltecV4Board::isLoRaFemLnaEnabled() const {
return loRaFEMControl.isLNAEnabled();
}
+3
View File
@@ -17,6 +17,9 @@ public:
void onAfterTransmit(void) override;
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1);
void powerOff() override;
bool setLoRaFemLnaEnabled(bool enable) override;
bool canControlLoRaFemLna() const override;
bool isLoRaFemLnaEnabled() const override;
uint16_t getBattMilliVolts() override;
const char* getManufacturerName() const override ;
+2 -1
View File
@@ -18,8 +18,9 @@ class LoRaFEMControl
void setRxModeEnable(void);
void setRxModeEnableWhenMCUSleep(void);
void setLNAEnable(bool enabled);
bool isLnaCanControl(void) { return lna_can_control; }
bool isLnaCanControl(void) const { return lna_can_control; }
void setLnaCanControl(bool can_control) { lna_can_control = can_control; }
bool isLNAEnabled(void) const { return lna_enabled; }
LoRaFEMType getFEMType(void) const { return fem_type; }
private:
LoRaFEMType fem_type=OTHER_FEM_TYPES;
+75 -1
View File
@@ -102,6 +102,28 @@ lib_deps =
${esp32_ota.lib_deps}
bakercp/CRC32 @ ^2.0.0
[env:heltec_v4_expansionkit_repeater]
extends = heltec_v4_oled
build_flags =
${heltec_v4_oled.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D ADVERT_NAME='"Heltec Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
-D ENV_PIN_SDA=4
-D ENV_PIN_SCL=3
build_src_filter = ${heltec_v4_oled.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_repeater>
lib_deps =
${heltec_v4_oled.lib_deps}
${esp32_ota.lib_deps}
bakercp/CRC32 @ ^2.0.0
[env:heltec_v4_repeater_bridge_espnow]
extends = heltec_v4_oled
build_flags =
@@ -200,6 +222,59 @@ lib_deps =
${heltec_v4_oled.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_companion_radio_ble_ps]
extends = heltec_v4_oled
platform_packages =
framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17
build_flags =
${heltec_v4_oled.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=SSD1306Display
-D BLE_PIN_CODE=123456 ; dynamic, random PIN
-D AUTO_SHUTDOWN_MILLIVOLTS=3400
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${heltec_v4_oled.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${heltec_v4_oled.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_3_companion_radio_ble_ps_femoff]
extends = heltec_v4_oled
platform_packages =
framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17
build_flags =
${heltec_v4_oled.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=SSD1306Display
-D BLE_PIN_CODE=123456 ; dynamic, random PIN
-D AUTO_SHUTDOWN_MILLIVOLTS=3400
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
-D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off
build_src_filter = ${heltec_v4_oled.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${heltec_v4_oled.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_companion_radio_wifi]
extends = heltec_v4_oled
build_flags =
@@ -207,7 +282,6 @@ build_flags =
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=SSD1306Display
-D WIFI_DEBUG_LOGGING=1
-D WIFI_SSID='"myssid"'
@@ -64,6 +64,29 @@ lib_deps =
densaugeo/base64 @ ~1.4.0
bakercp/CRC32 @ ^2.0.0
[env:Heltec_Wireless_Paper_companion_radio_ble_ps]
extends = Heltec_Wireless_Paper_base
platform_packages =
framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17
build_flags =
${Heltec_Wireless_Paper_base.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=E213Display
-D BLE_PIN_CODE=123456 ; dynamic, random PIN
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
build_src_filter = ${Heltec_Wireless_Paper_base.build_src_filter}
+<helpers/ui/E213Display.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_Wireless_Paper_base.lib_deps}
densaugeo/base64 @ ~1.4.0
bakercp/CRC32 @ ^2.0.0
[env:Heltec_Wireless_Paper_companion_radio_usb]
extends = Heltec_Wireless_Paper_base
build_flags =
@@ -0,0 +1,10 @@
#pragma once
#include <helpers/ESP32Board.h>
class LilygoT3S3SX1276Board : public ESP32Board {
public:
uint32_t getIRQGpio() override {
return P_LORA_DIO_0; // default for SX1276
}
};
+1 -1
View File
@@ -1,7 +1,7 @@
#include <Arduino.h>
#include "target.h"
ESP32Board board;
LilygoT3S3SX1276Board board;
#if defined(P_LORA_SCLK)
static SPIClass spi;
+2 -2
View File
@@ -3,7 +3,7 @@
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/ESP32Board.h>
#include <LilygoT3S3SX1276Board.h>
#include <helpers/radiolib/CustomSX1276Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
@@ -12,7 +12,7 @@
#include <helpers/ui/MomentaryButton.h>
#endif
extern ESP32Board board;
extern LilygoT3S3SX1276Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern SensorManager sensors;
+25
View File
@@ -146,6 +146,31 @@ lib_deps =
${LilyGo_TBeam_1W.lib_deps}
densaugeo/base64 @ ~1.4.0
; === LILYGO T-Beam 1W Companion Radio PS (BLE PS) ===
[env:LilyGo_TBeam_1W_companion_radio_ble_ps]
extends = LilyGo_TBeam_1W
platform_packages =
framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17
build_flags =
${LilyGo_TBeam_1W.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=256
-D PERSISTANT_GPS=1
-D ENV_SKIP_GPS_DETECT=1
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${LilyGo_TBeam_1W.build_src_filter}
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${LilyGo_TBeam_1W.lib_deps}
densaugeo/base64 @ ~1.4.0
; === LILYGO T-Beam 1W Companion Radio (WiFi) ===
[env:LilyGo_TBeam_1W_companion_radio_wifi]
extends = LilyGo_TBeam_1W
@@ -5,6 +5,13 @@ build_flags =
${esp32_base.build_flags}
-I variants/lilygo_tbeam_SX1262
-D TBEAM_SX1262
-D P_LORA_DIO_0=26
-D P_LORA_DIO_1=33
-D P_LORA_NSS=18
-D P_LORA_RESET=23
-D P_LORA_SCLK=5
-D P_LORA_MISO=19
-D P_LORA_MOSI=27
-D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
@@ -0,0 +1,10 @@
#pragma once
#include <helpers/esp32/TBeamBoard.h>
class LilygoTBeamSX1276Board : public TBeamBoard {
public:
uint32_t getIRQGpio() override {
return P_LORA_DIO_0; // default for SX1276
}
};
@@ -5,6 +5,13 @@ build_flags =
${esp32_base.build_flags}
-I variants/lilygo_tbeam_SX1276
-D TBEAM_SX1276
-D P_LORA_DIO_0=26
-D P_LORA_DIO_1=33
-D P_LORA_NSS=18
-D P_LORA_RESET=23
-D P_LORA_SCLK=5
-D P_LORA_MISO=19
-D P_LORA_MOSI=27
-D SX127X_CURRENT_LIMIT=120
-D RADIO_CLASS=CustomSX1276
-D WRAPPER_CLASS=CustomSX1276Wrapper
+1 -1
View File
@@ -1,7 +1,7 @@
#include <Arduino.h>
#include "target.h"
TBeamBoard board;
LilygoTBeamSX1276Board board;
#if defined(P_LORA_SCLK)
static SPIClass spi;
+2 -2
View File
@@ -3,7 +3,7 @@
#define RADIOLIB_STATIC_ONLY 1
//#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/esp32/TBeamBoard.h>
#include <LilygoTBeamSX1276Board.h>
#include <helpers/radiolib/CustomSX1276Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
@@ -12,7 +12,7 @@
#include <helpers/ui/MomentaryButton.h>
#endif
extern TBeamBoard board;
extern LilygoTBeamSX1276Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern EnvironmentSensorManager sensors;
@@ -5,6 +5,13 @@ build_flags =
${esp32_base.build_flags}
-I variants/lilygo_tbeam_supreme_SX1262
-D TBEAM_SUPREME_SX1262
-D P_LORA_DIO_0=26
-D P_LORA_DIO_1=33
-D P_LORA_NSS=18
-D P_LORA_RESET=23
-D P_LORA_SCLK=5
-D P_LORA_MISO=19
-D P_LORA_MOSI=27
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D USE_SX1262
@@ -21,4 +21,8 @@ public:
return (2 * raw);
}
uint32_t getIRQGpio() override {
return P_LORA_DIO_0; // default for SX1276
}
};
+1 -1
View File
@@ -6,7 +6,7 @@ build_flags = ${nrf52_base.build_flags}
${sensor_base.build_flags}
-I variants/rak3401
-D RAK_3401
-D NRF52_POWER_MANAGEMENT
; -D NRF52_POWER_MANAGEMENT
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
+1 -1
View File
@@ -7,7 +7,7 @@ build_flags = ${nrf52_base.build_flags}
-I variants/rak4631
-D RAK_4631
-D RAK_BOARD
-D NRF52_POWER_MANAGEMENT
; -D NRF52_POWER_MANAGEMENT
-D PIN_BOARD_SCL=14
-D PIN_BOARD_SDA=13
-D PIN_GPS_TX=PIN_SERIAL1_RX
+1 -1
View File
@@ -10,7 +10,7 @@ build_flags = ${nrf52_base.build_flags}
-I src/helpers/nrf52
-D NRF52_PLATFORM=1
-D USE_SX1262
-D NRF52_POWER_MANAGEMENT
; -D NRF52_POWER_MANAGEMENT
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D P_LORA_TX_LED=11
+22
View File
@@ -90,6 +90,28 @@ lib_deps =
${esp32_ota.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Xiao_C3_companion_radio_ble_ps]
extends = Xiao_esp32_C3
platform_packages =
framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17
build_src_filter = ${Xiao_esp32_C3.build_src_filter}
+<../examples/companion_radio/*.cpp>
+<helpers/esp32/*.cpp>
build_flags =
${Xiao_esp32_C3.build_flags}
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=256
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
lib_deps =
${Xiao_esp32_C3.lib_deps}
${esp32_ota.lib_deps}
densaugeo/base64 @ ~1.4.0
board_build.partitions = min_spiffs.csv ; get around 4mb flash limit
[env:Xiao_C3_companion_radio_usb]
extends = Xiao_esp32_C3
build_src_filter = ${Xiao_esp32_C3.build_src_filter}
+1 -1
View File
@@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags}
-I variants/xiao_nrf52
-UENV_INCLUDE_GPS
-D NRF52_PLATFORM
-D NRF52_POWER_MANAGEMENT
; -D NRF52_POWER_MANAGEMENT
-D XIAO_NRF52
-D USE_SX1262
-D RADIO_CLASS=CustomSX1262
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <Arduino.h>
#include <helpers/ESP32Board.h>
class XiaoS3Board : public ESP32Board {
public:
XiaoS3Board() { }
const char* getManufacturerName() const override {
return "Xiao S3";
}
};
+180
View File
@@ -0,0 +1,180 @@
[Xiao_S3]
extends = esp32_base
board = seeed_xiao_esp32s3
board_check = true
board_build.mcu = esp32s3
build_flags = ${esp32_base.build_flags}
${sensor_base.build_flags}
-I variants/xiao_s3
-UENV_INCLUDE_GPS
-D SEEED_XIAO_S3
-D PIN_VBAT_READ=1 ; D0
-D P_LORA_DIO_1=2 ; D1
-D P_LORA_NSS=5 ; D4
-D P_LORA_RESET=3 ; D2
-D P_LORA_BUSY=4 ; D3
-D P_LORA_SCLK=7 ; D8
-D P_LORA_MISO=8 ; D0
-D P_LORA_MOSI=9 ; D10
-D PIN_USER_BTN=-1 ; NC
-D PIN_STATUS_LED=21 ; Orange user led, LOW=On
-D PIN_BOARD_SDA=D6 ; D6=43
-D PIN_BOARD_SCL=D7 ; D7=44
-D SX126X_RXEN=6 ; D5
-D SX126X_TXEN=RADIOLIB_NC
-D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D SX126X_RX_BOOSTED_GAIN=1
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/xiao_s3>
+<helpers/sensors>
lib_deps =
${esp32_base.lib_deps}
${sensor_base.lib_deps}
[env:Xiao_S3_repeater]
extends = Xiao_S3
build_src_filter = ${Xiao_S3.build_src_filter}
+<../examples/simple_repeater/*.cpp>
build_flags =
${Xiao_S3.build_flags}
-D ADVERT_NAME='"Xiao S3 Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
lib_deps =
${Xiao_S3.lib_deps}
${esp32_ota.lib_deps}
[env:Xiao_S3_repeater_bridge_espnow]
extends = Xiao_S3
build_src_filter = ${Xiao_S3.build_src_filter}
+<helpers/bridges/ESPNowBridge.cpp>
+<../examples/simple_repeater/*.cpp>
build_flags =
${Xiao_S3.build_flags}
-D ADVERT_NAME='"ESPNow Bridge"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D WITH_ESPNOW_BRIDGE=1
; -D BRIDGE_DEBUG=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
lib_deps =
${Xiao_S3.lib_deps}
${esp32_ota.lib_deps}
[env:Xiao_S3_room_server]
extends = Xiao_S3
build_src_filter = ${Xiao_S3.build_src_filter}
+<../examples/simple_room_server>
build_flags =
${Xiao_S3.build_flags}
-D ADVERT_NAME='"Xiao S3 Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
lib_deps =
${Xiao_S3.lib_deps}
${esp32_ota.lib_deps}
[env:Xiao_S3_companion_radio_ble]
extends = Xiao_S3
build_flags =
${Xiao_S3.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D DISPLAY_CLASS=SSD1306Display
-D OFFLINE_QUEUE_SIZE=256
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Xiao_S3.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Xiao_S3.lib_deps}
densaugeo/base64 @ ~1.4.0
adafruit/Adafruit SSD1306 @ ^2.5.13
[env:Xiao_S3_companion_radio_ble_ps]
extends = Xiao_S3
platform_packages =
framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17
build_flags =
${Xiao_S3.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D DISPLAY_CLASS=SSD1306Display
-D OFFLINE_QUEUE_SIZE=256
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Xiao_S3.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Xiao_S3.lib_deps}
densaugeo/base64 @ ~1.4.0
adafruit/Adafruit SSD1306 @ ^2.5.13
[env:Xiao_S3_companion_radio_usb]
extends = Xiao_S3
build_flags =
${Xiao_S3.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=SSD1306Display
-D OFFLINE_QUEUE_SIZE=256
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Xiao_S3.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Xiao_S3.lib_deps}
densaugeo/base64 @ ~1.4.0
adafruit/Adafruit SSD1306 @ ^2.5.13
[env:Xiao_S3_sensor]
extends = Xiao_S3
build_src_filter = ${Xiao_S3.build_src_filter}
+<../examples/simple_sensor>
build_flags =
${Xiao_S3.build_flags}
-D ADVERT_NAME='"Xiao S3 Sensor"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
lib_deps =
${Xiao_S3.lib_deps}
${esp32_ota.lib_deps}
+56
View File
@@ -0,0 +1,56 @@
#include <Arduino.h>
#include "target.h"
XiaoS3Board board;
#if defined(P_LORA_SCLK)
static SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
#else
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
#endif
WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
EnvironmentSensorManager sensors;
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif
bool radio_init() {
fallback_clock.begin();
rtc_clock.begin(Wire);
pinMode(21, INPUT);
pinMode(48, OUTPUT);
#if defined(P_LORA_SCLK)
spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
return radio.std_init(&spi);
#else
return radio.std_init();
#endif
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/ESP32Board.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/SSD1306Display.h>
#include <helpers/ui/MomentaryButton.h>
#endif
#include "XiaoS3Board.h"
extern XiaoS3Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern EnvironmentSensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();
+26
View File
@@ -173,6 +173,32 @@ lib_deps =
densaugeo/base64 @ ~1.4.0
adafruit/Adafruit SSD1306 @ ^2.5.13
[env:Xiao_S3_WIO_companion_radio_ble_ps]
extends = Xiao_S3_WIO
platform_packages =
framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17
build_flags =
${Xiao_S3_WIO.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D DISPLAY_CLASS=SSD1306Display
-D OFFLINE_QUEUE_SIZE=256
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Xiao_S3_WIO.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Xiao_S3_WIO.lib_deps}
densaugeo/base64 @ ~1.4.0
adafruit/Adafruit SSD1306 @ ^2.5.13
[env:Xiao_S3_WIO_companion_radio_serial]
extends = Xiao_S3_WIO
build_flags =