diff --git a/build-iotthinks.sh b/build-iotthinks.sh new file mode 100644 index 0000000000..7c654482c7 --- /dev/null +++ b/build-iotthinks.sh @@ -0,0 +1,96 @@ +# sh ./build-repeaters-iotthinks.sh +export FIRMWARE_VERSION="PowerSaving15" + +############# Repeaters ############# +# Commonly-used boards +## ESP32 - 12 boards +sh build.sh build-firmware \ +Heltec_v3_repeater \ +Heltec_WSL3_repeater \ +heltec_v4_repeater \ +Station_G2_repeater \ +T_Beam_S3_Supreme_SX1262_repeater \ +Tbeam_SX1262_repeater \ +LilyGo_T3S3_sx1262_repeater \ +Xiao_S3_WIO_repeater \ +Xiao_C3_repeater \ +Xiao_C6_repeater_ \ +Heltec_E290_repeater \ +Heltec_Wireless_Tracker_repeater + +## NRF52 - 13 boards +sh build.sh build-firmware \ +RAK_4631_repeater \ +Heltec_t114_repeater \ +Xiao_nrf52_repeater \ +Heltec_mesh_solar_repeater \ +ProMicro_repeater \ +SenseCap_Solar_repeater \ +t1000e_repeater \ +LilyGo_T-Echo_repeater \ +WioTrackerL1_repeater \ +RAK_3401_repeater \ +RAK_WisMesh_Tag_repeater \ +GAT562_30S_Mesh_Kit_repeater \ +GAT562_Mesh_Tracker_Pro_repeater + +## ESP32, SX1276 - 3 boards +sh build.sh build-firmware \ +Heltec_v2_repeater \ +LilyGo_TLora_V2_1_1_6_repeater \ +Tbeam_SX1276_repeater + +## Ikoka - 3 boards +sh build.sh build-firmware \ +ikoka_nano_nrf_22dbm_repeater \ +ikoka_nano_nrf_30dbm_repeater \ +ikoka_nano_nrf_33dbm_repeater + +############# Room Server ############# +# ESP32 +sh build.sh build-firmware \ +Heltec_v3_room_server \ +heltec_v4_room_server + +# NRF52 +sh build.sh build-firmware \ +RAK_4631_room_server \ +Heltec_t114_room_server \ +Xiao_nrf52_room_server \ +t1000e_room_server \ +WioTrackerL1_room_server \ +RAK_3401_room_server + +############# Companions BLE ############# +# ESP32 +sh build.sh build-firmware \ +Heltec_v3_companion_radio_ble_ps \ +heltec_v4_companion_radio_ble_ps \ +heltec_v4_companion_radio_ble_ps_femoff \ +Xiao_S3_WIO_companion_radio_ble \ +Heltec_Wireless_Paper_companion_radio_ble + +# NRF52 +sh build.sh build-firmware \ +RAK_4631_companion_radio_ble \ +Heltec_t114_companion_radio_ble \ +Xiao_nrf52_companion_radio_ble \ +t1000e_companion_radio_ble \ +LilyGo_T-Echo_companion_radio_ble \ +WioTrackerL1_companion_radio_ble \ +RAK_3401_companion_radio_ble \ +RAK_WisMesh_Tag_companion_radio_ble + +############# Companions USB ############# +sh build.sh build-firmware \ +Heltec_v3_companion_radio_usb + +############# Companions BLE PS ############# +sh build.sh build-firmware \ +Heltec_v3_companion_radio_ble_ps \ +heltec_v4_companion_radio_ble_ps \ +heltec_v4_3_companion_radio_ble_ps_femoff \ +Xiao_C3_companion_radio_ble_ps \ +Xiao_S3_companion_radio_ble_ps \ +Xiao_S3_WIO_companion_radio_ble_ps \ +Heltec_v2_companion_radio_ble_ps diff --git a/build.sh b/build.sh index 313c4c47a0..9ef20d11ac 100755 --- a/build.sh +++ b/build.sh @@ -1,9 +1,41 @@ #!/usr/bin/env bash +ALL_PIO_ENVS=() +PIO_CONFIG_JSON="" +MENU_CHOICE="" +SELECTED_TARGET="" + +ENV_VARIANT_SUFFIX_PATTERN='companion_radio_serial|companion_radio_wifi|companion_radio_usb|comp_radio_usb|companion_usb|companion_radio_ble|companion_ble|repeater_bridge_rs232_serial1|repeater_bridge_rs232_serial2|repeater_bridge_rs232|repeater_bridge_espnow|terminal_chat|room_server|room_svr|kiss_modem|sensor|repeatr|repeater' +BOARD_MODIFIER_WITHOUT_DISPLAY="_without_display" +BOARD_MODIFIER_LOGGING="_logging" +BOARD_MODIFIER_TFT="_tft" +BOARD_MODIFIER_EINK="_eink" +BOARD_MODIFIER_EINK_SUFFIX="Eink" +BOARD_LABEL_WITHOUT_DISPLAY="without_display" +BOARD_LABEL_LOGGING="logging" +BOARD_LABEL_TFT="tft" +BOARD_LABEL_EINK="eink" +DEFAULT_VARIANT_LABEL="default" +TAG_PREFIX_ROOM_SERVER="room-server" +TAG_PREFIX_COMPANION="companion" +TAG_PREFIX_REPEATER="repeater" +BULK_BUILD_SUFFIX_REPEATER="_repeater" +BULK_BUILD_SUFFIX_COMPANION_USB="_companion_radio_usb" +BULK_BUILD_SUFFIX_COMPANION_BLE="_companion_radio_ble" +BULK_BUILD_SUFFIX_ROOM_SERVER="_room_server" +SUPPORTED_PLATFORM_PATTERN='ESP32_PLATFORM|NRF52_PLATFORM|STM32_PLATFORM|RP2040_PLATFORM' +OUTPUT_DIR="out" +FALLBACK_VERSION_PREFIX="dev" +FALLBACK_VERSION_DATE_FORMAT='+%Y-%m-%d-%H-%M' + +# External programs invoked by this script: +# bash, cat, cp, date, git, grep, head, mkdir, pio, python3, rm, sed, sort +# Keep this list in sync when adding or removing non-builtin command usage. + global_usage() { cat - < [target] +bash build.sh [target] Commands: help|usage|-h|--help: Shows this message. @@ -17,21 +49,26 @@ Commands: Examples: Build firmware for the "RAK_4631_repeater" device target -$ sh build.sh build-firmware RAK_4631_repeater +$ bash build.sh build-firmware RAK_4631_repeater + +Run without arguments to choose a target from an interactive menu +$ bash build.sh Build all firmwares for device targets containing the string "RAK_4631" -$ sh build.sh build-matching-firmwares +$ bash build.sh build-matching-firmwares Build all companion firmwares -$ sh build.sh build-companion-firmwares +$ bash build.sh build-companion-firmwares Build all repeater firmwares -$ sh build.sh build-repeater-firmwares +$ bash build.sh build-repeater-firmwares Build all chat room server firmwares -$ sh build.sh build-room-server-firmwares +$ bash build.sh build-room-server-firmwares Environment Variables: + FIRMWARE_VERSION=vX.Y.Z: Firmware version to embed in the build output. + If not set, build.sh derives a default from the latest matching git tag and appends "-dev". DISABLE_DEBUG=1: Disables all debug logging flags (MESH_DEBUG, MESH_PACKET_LOGGING, etc.) If not set, debug flags from variant platformio.ini files are used. @@ -39,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:" +init_project_context() { + if [ ${#ALL_PIO_ENVS[@]} -eq 0 ]; then + mapfile -t ALL_PIO_ENVS < <(pio project config | grep 'env:' | sed 's/env://') + fi + + if [ -z "$PIO_CONFIG_JSON" ]; then + PIO_CONFIG_JSON=$(pio project config --json-output) + fi +} + get_pio_envs() { - pio project config | grep 'env:' | sed 's/env://' + if [ ${#ALL_PIO_ENVS[@]} -gt 0 ]; then + printf '%s\n' "${ALL_PIO_ENVS[@]}" + else + pio project config | grep 'env:' | sed 's/env://' + fi +} + +canonicalize_variant_suffix() { + local variant_suffix=$1 + + case "${variant_suffix,,}" in + comp_radio_usb|companion_usb|companion_radio_usb) + echo "companion_radio_usb" + ;; + companion_ble|companion_radio_ble) + echo "companion_radio_ble" + ;; + room_svr|room_server) + echo "room_server" + ;; + repeatr|repeater) + echo "repeater" + ;; + *) + echo "${variant_suffix,,}" + ;; + esac +} + +trim_trailing_underscores() { + local value=$1 + + while [[ "$value" == *_ ]]; do + value=${value%_} + done + + echo "$value" +} + +sort_lines_case_insensitive() { + sort -f +} + +print_numbered_menu() { + local items=("$@") + local i + + for i in "${!items[@]}"; do + printf '%d) %s\n' "$((i + 1))" "${items[$i]}" + done +} + +prompt_menu_choice() { + local prompt_label=$1 + local max_choice=$2 + local allow_back=${3:-0} + local choice + + while true; do + if [ "$allow_back" -eq 1 ]; then + read -r -p "${prompt_label} [1-${max_choice}, B=Back, Q=Quit]: " choice + else + read -r -p "${prompt_label} [1-${max_choice}, Q=Quit]: " choice + fi + + case "${choice^^}" in + Q) + MENU_CHOICE="QUIT" + return 0 + ;; + B) + if [ "$allow_back" -eq 1 ]; then + MENU_CHOICE="BACK" + return 0 + fi + echo "Invalid selection." + ;; + *) + if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "$max_choice" ]; then + MENU_CHOICE="$choice" + return 0 + fi + echo "Invalid selection." + ;; + esac + done +} + +get_env_metadata() { + local env_name=$1 + local trimmed_env_name + local board_part + local variant_part + local board_family + local board_modifier + local variant_label + local tag_prefix + + trimmed_env_name=$(trim_trailing_underscores "$env_name") + board_part=$trimmed_env_name + variant_part="" + + shopt -s nocasematch + # Split a raw env name into board and variant pieces using the normalized + # suffix vocabulary defined near the top of the file. + if [[ "$trimmed_env_name" =~ ^(.+)[_-](${ENV_VARIANT_SUFFIX_PATTERN})$ ]]; then + board_part=${BASH_REMATCH[1]} + variant_part=$(canonicalize_variant_suffix "${BASH_REMATCH[2]}") + fi + + # Fold display and form-factor suffixes into the variant label so related + # boards share one first-level menu entry. + case "$board_part" in + *"$BOARD_MODIFIER_WITHOUT_DISPLAY") + board_family=${board_part%"$BOARD_MODIFIER_WITHOUT_DISPLAY"} + board_modifier="$BOARD_LABEL_WITHOUT_DISPLAY" + ;; + *"$BOARD_MODIFIER_LOGGING") + board_family=${board_part%"$BOARD_MODIFIER_LOGGING"} + board_modifier="$BOARD_LABEL_LOGGING" + ;; + *"$BOARD_MODIFIER_TFT") + board_family=${board_part%"$BOARD_MODIFIER_TFT"} + board_modifier="$BOARD_LABEL_TFT" + ;; + *"$BOARD_MODIFIER_EINK") + board_family=${board_part%"$BOARD_MODIFIER_EINK"} + board_modifier="$BOARD_LABEL_EINK" + ;; + *"$BOARD_MODIFIER_EINK_SUFFIX") + board_family=${board_part%"$BOARD_MODIFIER_EINK_SUFFIX"} + board_modifier="$BOARD_LABEL_EINK" + ;; + *) + board_family=$board_part + board_modifier="" + ;; + esac + shopt -u nocasematch + + variant_label="$variant_part" + if [ -n "$board_modifier" ]; then + if [ -n "$variant_label" ]; then + variant_label="${board_modifier}_${variant_label}" + else + variant_label="$board_modifier" + fi + fi + + if [ -z "$variant_label" ]; then + variant_label="$DEFAULT_VARIANT_LABEL" + fi + + case "$variant_part" in + room_server) + tag_prefix="$TAG_PREFIX_ROOM_SERVER" + ;; + companion_radio_*) + tag_prefix="$TAG_PREFIX_COMPANION" + ;; + repeater*) + tag_prefix="$TAG_PREFIX_REPEATER" + ;; + *) + tag_prefix="" + ;; + esac + + printf '%s\t%s\t%s\n' "$board_family" "$variant_label" "$tag_prefix" +} + +get_metadata_field() { + local env_name=$1 + local field_index=$2 + local metadata + + metadata=$(get_env_metadata "$env_name") + case "$field_index" in + 1) + echo "${metadata%%$'\t'*}" + ;; + 2) + metadata=${metadata#*$'\t'} + echo "${metadata%%$'\t'*}" + ;; + 3) + echo "${metadata##*$'\t'}" + ;; + esac } -# Catch cries for help before doing anything else. -case $1 in - help|usage|-h|--help) +get_board_family_for_env() { + get_metadata_field "$1" 1 +} + +get_variant_name_for_env() { + get_metadata_field "$1" 2 +} + +get_release_tag_prefix_for_env() { + get_metadata_field "$1" 3 +} + +get_variants_for_board() { + local board_family=$1 + local env + + for env in "${ALL_PIO_ENVS[@]}"; do + if [ "$(get_board_family_for_env "$env")" == "$board_family" ]; then + echo "$env" + fi + done | sort_lines_case_insensitive +} + +prompt_for_variant_for_board() { + local board=$1 + local -A seen_variant_labels=() + local variants + local variant_labels + local i + local j + + mapfile -t variants < <(get_variants_for_board "$board") + if [ ${#variants[@]} -eq 0 ]; then + echo "No firmware variants were found for ${board}." + return 1 + fi + + if [ ${#variants[@]} -eq 1 ]; then + SELECTED_TARGET="${variants[0]}" + return 0 + fi + + variant_labels=() + for i in "${!variants[@]}"; do + variant_labels[i]=$(get_variant_name_for_env "${variants[$i]}") + seen_variant_labels["${variant_labels[$i]}"]=$(( ${seen_variant_labels["${variant_labels[$i]}"]:-0} + 1 )) + done + + # Stop early if normalization would present the user with ambiguous labels. + for i in "${!variant_labels[@]}"; do + if [ "${seen_variant_labels["${variant_labels[$i]}"]}" -gt 1 ]; then + echo "Ambiguous firmware variants detected for ${board}: ${variant_labels[$i]}" + echo "The normalized menu labels are not unique for this board family." + for j in "${!variants[@]}"; do + echo " ${variants[$j]}" + done + exit 1 + fi + done + + echo "Select a firmware variant for ${board}:" + while true; do + print_numbered_menu "${variant_labels[@]}" + prompt_menu_choice "Variant selection" "${#variant_labels[@]}" 1 + if [ "$MENU_CHOICE" == "BACK" ]; then + return 1 + fi + if [ "$MENU_CHOICE" == "QUIT" ]; then + echo "Cancelled." + exit 1 + fi + + SELECTED_TARGET="${variants[$((MENU_CHOICE - 1))]}" + return 0 + done +} + +prompt_for_board_target() { + local -A seen_boards=() + local boards=() + local board + local env + + if ! [ -t 0 ]; then + echo "No command provided and no interactive terminal is available." global_usage exit 1 - ;; - list|-l) - get_pio_envs - exit 0 - ;; -esac + fi -# cache project config json for use in get_platform_for_env() -PIO_CONFIG_JSON=$(pio project config --json-output) + if [ ${#ALL_PIO_ENVS[@]} -eq 0 ]; then + echo "No PlatformIO environments were found." + exit 1 + fi + + for env in "${ALL_PIO_ENVS[@]}"; do + board=$(get_board_family_for_env "$env") + if [ -z "${seen_boards[$board]}" ]; then + seen_boards["$board"]=1 + boards+=("$board") + fi + done + + mapfile -t boards < <(printf '%s\n' "${boards[@]}" | sort_lines_case_insensitive) + + echo "No command provided. Select a board family:" + while true; do + print_numbered_menu "${boards[@]}" + prompt_menu_choice "Board selection" "${#boards[@]}" + if [ "$MENU_CHOICE" == "QUIT" ]; then + echo "Cancelled." + exit 1 + fi + + board=${boards[$((MENU_CHOICE - 1))]} + if prompt_for_variant_for_board "$board"; then + echo "Building firmware for ${SELECTED_TARGET}" + return 0 + fi + done +} + +get_latest_version_from_tags() { + local env_name=$1 + local tag_prefix + local latest_tag + local fallback_version + + fallback_version="${FALLBACK_VERSION_PREFIX}-$(date "${FALLBACK_VERSION_DATE_FORMAT}")" + tag_prefix=$(get_release_tag_prefix_for_env "$env_name") + if [ -z "$tag_prefix" ]; then + echo "$fallback_version" + return 0 + fi + + latest_tag=$(git tag --list "${tag_prefix}-v*" --sort=-version:refname | head -n 1) + if [ -z "$latest_tag" ]; then + echo "$fallback_version" + return 0 + fi + + echo "${latest_tag#"${tag_prefix}"-}" +} + +derive_default_firmware_version() { + local env_name=$1 + local base_version + + base_version=$(get_latest_version_from_tags "$env_name") + case "$base_version" in + *-dev|dev-*) + echo "$base_version" + ;; + *) + echo "${base_version}-dev" + ;; + esac +} + +prompt_for_firmware_version() { + local env_name=$1 + local suggested_version + local entered_version + + suggested_version=$(derive_default_firmware_version "$env_name") + + if ! [ -t 0 ]; then + FIRMWARE_VERSION="$suggested_version" + echo "FIRMWARE_VERSION not set, using derived default: ${FIRMWARE_VERSION}" + return 0 + fi + + echo "Suggested firmware version for ${env_name}: ${suggested_version}" + read -r -e -i "${suggested_version}" -p "Firmware version: " entered_version + FIRMWARE_VERSION="${entered_version:-$suggested_version}" +} -# $1 should be the string to find (case insensitive) get_pio_envs_containing_string() { + local env + shopt -s nocasematch - envs=($(get_pio_envs)) - for env in "${envs[@]}"; do - if [[ "$env" == *${1}* ]]; then - echo $env - fi + for env in "${ALL_PIO_ENVS[@]}"; do + if [[ "$env" == *${1}* ]]; then + echo "$env" + fi done + shopt -u nocasematch } -# $1 should be the string to find (case insensitive) get_pio_envs_ending_with_string() { + local env + shopt -s nocasematch - envs=($(get_pio_envs)) - for env in "${envs[@]}"; do + for env in "${ALL_PIO_ENVS[@]}"; do if [[ "$env" == *${1} ]]; then - echo $env + echo "$env" fi done + shopt -u nocasematch } -# get platform flag for a given environment -# $1 should be the environment name get_platform_for_env() { local env_name=$1 - 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 -build_firmware() { - # get env platform for post build actions - ENV_PLATFORM=($(get_platform_for_env $1)) +copy_build_output() { + local source_path=$1 + local output_path=$2 - # get git commit sha - COMMIT_HASH=$(git rev-parse --short HEAD) + if [ -f "$source_path" ]; then + cp -- "$source_path" "$output_path" + fi +} - # set firmware build date - FIRMWARE_BUILD_DATE=$(date '+%d-%b-%Y') +collect_esp32_artifacts() { + local env_name=$1 + local firmware_filename=$2 - # get FIRMWARE_VERSION, which should be provided by the environment - if [ -z "$FIRMWARE_VERSION" ]; then - echo "FIRMWARE_VERSION must be set in environment" - exit 1 - fi + pio run -t mergebin -e "$env_name" + copy_build_output ".pio/build/${env_name}/firmware.bin" "out/${firmware_filename}.bin" + copy_build_output ".pio/build/${env_name}/firmware-merged.bin" "out/${firmware_filename}-merged.bin" +} - # set firmware version string - # e.g: v1.0.0-abcdef - FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}-${COMMIT_HASH}" +collect_nrf52_artifacts() { + local env_name=$1 + local firmware_filename=$2 - # craft filename - # e.g: RAK_4631_Repeater-v1.0.0-SHA - FIRMWARE_FILENAME="$1-${FIRMWARE_VERSION_STRING}" + python3 bin/uf2conv/uf2conv.py ".pio/build/${env_name}/firmware.hex" -c -o ".pio/build/${env_name}/firmware.uf2" -f 0xADA52840 + copy_build_output ".pio/build/${env_name}/firmware.uf2" "out/${firmware_filename}.uf2" + copy_build_output ".pio/build/${env_name}/firmware.zip" "out/${firmware_filename}.zip" +} - # add firmware version info to end of existing platformio build flags in environment vars - export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DFIRMWARE_BUILD_DATE='\"${FIRMWARE_BUILD_DATE}\"' -DFIRMWARE_VERSION='\"${FIRMWARE_VERSION_STRING}\"'" +collect_stm32_artifacts() { + local env_name=$1 + local firmware_filename=$2 - # disable debug flags if requested - disable_debug_flags + copy_build_output ".pio/build/${env_name}/firmware.bin" "out/${firmware_filename}.bin" + copy_build_output ".pio/build/${env_name}/firmware.hex" "out/${firmware_filename}.hex" +} - # build firmware target - pio run -e $1 +collect_rp2040_artifacts() { + local env_name=$1 + local firmware_filename=$2 - # build merge-bin for esp32 fresh install, copy .bins to out folder (e.g: Heltec_v3_room_server-v1.0.0-SHA.bin) - if [ "$ENV_PLATFORM" == "ESP32_PLATFORM" ]; then - pio run -t mergebin -e $1 - cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true - cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-merged.bin 2>/dev/null || true - fi + copy_build_output ".pio/build/${env_name}/firmware.bin" "out/${firmware_filename}.bin" + copy_build_output ".pio/build/${env_name}/firmware.uf2" "out/${firmware_filename}.uf2" +} - # build .uf2 for nrf52 boards, copy .uf2 and .zip to out folder (e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2) - if [ "$ENV_PLATFORM" == "NRF52_PLATFORM" ]; then - python3 bin/uf2conv/uf2conv.py .pio/build/$1/firmware.hex -c -o .pio/build/$1/firmware.uf2 -f 0xADA52840 - cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true - cp .pio/build/$1/firmware.zip out/${FIRMWARE_FILENAME}.zip 2>/dev/null || true - fi +collect_build_artifacts() { + local env_name=$1 + local env_platform=$2 + local firmware_filename=$3 + + # Post-build outputs differ by platform, so dispatch to the matching + # collector after the main firmware build succeeds. + case "$env_platform" in + ESP32_PLATFORM) + collect_esp32_artifacts "$env_name" "$firmware_filename" + ;; + NRF52_PLATFORM) + collect_nrf52_artifacts "$env_name" "$firmware_filename" + ;; + STM32_PLATFORM) + collect_stm32_artifacts "$env_name" "$firmware_filename" + ;; + RP2040_PLATFORM) + collect_rp2040_artifacts "$env_name" "$firmware_filename" + ;; + esac +} - # for stm32, copy .bin and .hex to out folder - if [ "$ENV_PLATFORM" == "STM32_PLATFORM" ]; then - cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true - cp .pio/build/$1/firmware.hex out/${FIRMWARE_FILENAME}.hex 2>/dev/null || true +build_firmware() { + local env_name=$1 + local env_platform + local commit_hash + local firmware_build_date + local firmware_version_string + local firmware_filename + + env_platform=$(get_platform_for_env "$env_name") + if ! is_supported_platform "$env_platform"; then + echo "Unsupported or unknown platform for env: $env_name" + exit 1 fi - # for rp2040, copy .bin and .uf2 to out folder - if [ "$ENV_PLATFORM" == "RP2040_PLATFORM" ]; then - cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true - cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true + commit_hash=$(git rev-parse --short HEAD) + firmware_build_date=$(date '+%d-%b-%Y') + + if [ -z "$FIRMWARE_VERSION" ]; then + prompt_for_firmware_version "$env_name" + echo "Using firmware version: ${FIRMWARE_VERSION}" fi + firmware_version_string="${FIRMWARE_VERSION}-${commit_hash}" + firmware_filename="${env_name}-${firmware_version_string}" + + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DFIRMWARE_BUILD_DATE='\"${firmware_build_date}\"' -DFIRMWARE_VERSION='\"${firmware_version_string}\"'" + disable_debug_flags + + pio run -e "$env_name" + collect_build_artifacts "$env_name" "$env_platform" "$firmware_filename" } -# firmwares containing $1 will be built build_all_firmwares_matching() { - envs=($(get_pio_envs_containing_string "$1")) + local envs + local env + + mapfile -t envs < <(get_pio_envs_containing_string "$1") for env in "${envs[@]}"; do - build_firmware $env + build_firmware "$env" done } -# firmwares ending with $1 will be built build_all_firmwares_by_suffix() { - envs=($(get_pio_envs_ending_with_string "$1")) + local envs + local env + + mapfile -t envs < <(get_pio_envs_ending_with_string "$1") for env in "${envs[@]}"; do - build_firmware $env + build_firmware "$env" done } build_repeater_firmwares() { - -# # build specific repeater firmwares -# build_firmware "Heltec_v2_repeater" -# build_firmware "Heltec_v3_repeater" -# build_firmware "Xiao_C3_Repeater_sx1262" -# build_firmware "Xiao_S3_WIO_Repeater" -# build_firmware "LilyGo_T3S3_sx1262_Repeater" -# build_firmware "RAK_4631_Repeater" - - # build all repeater firmwares - build_all_firmwares_by_suffix "_repeater" - + build_all_firmwares_by_suffix "$BULK_BUILD_SUFFIX_REPEATER" } build_companion_firmwares() { - -# # build specific companion firmwares -# build_firmware "Heltec_v2_companion_radio_usb" -# build_firmware "Heltec_v2_companion_radio_ble" -# build_firmware "Heltec_v3_companion_radio_usb" -# build_firmware "Heltec_v3_companion_radio_ble" -# build_firmware "Xiao_S3_WIO_companion_radio_ble" -# build_firmware "LilyGo_T3S3_sx1262_companion_radio_usb" -# build_firmware "LilyGo_T3S3_sx1262_companion_radio_ble" -# build_firmware "RAK_4631_companion_radio_usb" -# build_firmware "RAK_4631_companion_radio_ble" -# build_firmware "t1000e_companion_radio_ble" - - # build all companion firmwares - build_all_firmwares_by_suffix "_companion_radio_usb" - build_all_firmwares_by_suffix "_companion_radio_ble" - + build_all_firmwares_by_suffix "$BULK_BUILD_SUFFIX_COMPANION_USB" + build_all_firmwares_by_suffix "$BULK_BUILD_SUFFIX_COMPANION_BLE" } build_room_server_firmwares() { - -# # build specific room server firmwares -# build_firmware "Heltec_v3_room_server" -# build_firmware "RAK_4631_room_server" - - # build all room server firmwares - build_all_firmwares_by_suffix "_room_server" - + build_all_firmwares_by_suffix "$BULK_BUILD_SUFFIX_ROOM_SERVER" } build_firmwares() { @@ -245,34 +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 - echo "usage: $0 build-firmware " + if [ -z "$output_dir" ] || [ "$output_dir" == "/" ] || [ "$output_dir" == "." ]; then + echo "Refusing to clean unsafe output directory: $output_dir" exit 1 fi -elif [[ $1 == "build-matching-firmwares" ]]; then - if [ "$2" ]; then - build_all_firmwares_matching $2 - else - echo "usage: $0 build-matching-firmwares " + + rm -rf -- "$output_dir" + mkdir -p -- "$output_dir" +} + +run_build_firmware_command() { + local targets=("${@:2}") + local env + + if [ ${#targets[@]} -eq 0 ]; then + echo "usage: $0 build-firmware " exit 1 fi -elif [[ $1 == "build-firmwares" ]]; then - build_firmwares -elif [[ $1 == "build-companion-firmwares" ]]; then - build_companion_firmwares -elif [[ $1 == "build-repeater-firmwares" ]]; then - build_repeater_firmwares -elif [[ $1 == "build-room-server-firmwares" ]]; then - build_room_server_firmwares -fi + + for env in "${targets[@]}"; do + build_firmware "$env" + done +} + +run_command() { + case "$1" in + build-firmware) + run_build_firmware_command "$@" + ;; + build-matching-firmwares) + if [ -n "$2" ]; then + build_all_firmwares_matching "$2" + else + echo "usage: $0 build-matching-firmwares " + exit 1 + fi + ;; + build-firmwares) + build_firmwares + ;; + build-companion-firmwares) + build_companion_firmwares + ;; + build-repeater-firmwares) + build_repeater_firmwares + ;; + build-room-server-firmwares) + build_room_server_firmwares + ;; + *) + global_usage + exit 1 + ;; + esac +} + +main() { + case "${1:-}" in + help|usage|-h|--help) + global_usage + exit 0 + ;; + list|-l) + init_project_context + get_pio_envs + exit 0 + ;; + esac + + init_project_context + + if [ $# -eq 0 ]; then + prompt_for_board_target + set -- build-firmware "$SELECTED_TARGET" + fi + + prepare_output_dir + run_command "$@" +} + +main "$@" diff --git a/docs/cli_commands.md b/docs/cli_commands.md index fb698228ed..bf77872f84 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -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 ` +- `get recent.repeater page ` +- `set recent.repeater ` + +**Parameters:** +- `prefix_hex_6`: Exactly 3 bytes of next-hop prefix in hex (6 chars) +- `snr_db`: SNR in dB (supports decimals; stored at x4 precision) +- `page`: 1-based page number + +**Notes:** +- `set` stores or updates the prefix in the recent repeater table. +- Rows are sorted by prefix width (3-byte, 2-byte, 1-byte), then SNR descending. +- A full direct retry failure lowers the stored SNR by `0.25 dB`. +- If a full failure has no row yet, it first seeds the row at the active retry cutoff + `2.5 dB`, then applies the `0.25 dB` penalty. +- Serial CLI page size is fixed at `128` rows; choose page with `get recent.repeater `. +- Over LoRa remote CLI, page size is fixed at `7` rows; choose page with `get recent.repeater `. + +--- + ## 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 ` + +**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,98 @@ 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 ` + +**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 the SNR margin used for direct retry eligibility +**Usage:** +- `get direct.retry.margin` +- `set direct.retry.margin ` + +**Parameters:** +- `value`: Rooftop preset margin in dB above the SF-specific receive floor (minimum `0`, maximum `40`, quarter-dB precision, default `5.0`) + +**Default:** `5.0` + +**Note:** `get direct.retry.margin` returns the active preset's effective margin. The retry gate uses the active SF floor of `SF5=-2.5`, `SF6=-5`, `SF7=-7.5`, `SF8=-10`, `SF9=-12.5`, `SF10=-15`, `SF11=-17.5`, `SF12=-20`, then adds this margin. + +--- + +#### View or change the direct retry timing preset +**Usage:** +- `get direct.retry.preset` +- `set direct.retry.preset ` + +**Parameters:** +- `value`: `infra`|`rooftop`|`mobile` or `0`|`1`|`2` + +**Default:** `rooftop` (`1`) + +**Presets:** +- `infra` (`0`): `275 ms` base wait, `4` retries, `150 ms` added per retry, SNR gate is SF floor + `15 dB` +- `rooftop` (`1`): `175 ms` base wait, `15` retries, `100 ms` added per retry, SNR gate is SF floor + `5 dB` +- `mobile` (`2`): `175 ms` base wait, `15` retries, `50 ms` added per retry, SNR gate is the SF floor + +**Note:** Selecting a preset copies those values into the retry settings. You can refine `direct.retry.margin`, `direct.retry.count`, `direct.retry.base`, or `direct.retry.step` afterward. Retry delay is `direct.txdelay` jitter + base wait + packet-length airtime wait + per-attempt step. + +--- + +#### View or change the number of direct retry attempts +**Usage:** +- `get direct.retry.count` +- `set direct.retry.count ` + +**Parameters:** +- `value`: Maximum retry attempts after initial TX (`1`-`15`) + +**Default:** `15` + +**Note:** The effective value is capped by total direct path length: paths of `3` hops or less use at most `8` retries, `4` hops use at most `12`, and `5+` hops use at most `15`. A queued resend is canceled early when the next-hop echo is heard. + +--- + +#### View or change the base direct retry wait (milliseconds) +**Usage:** +- `get direct.retry.base` +- `set direct.retry.base ` + +**Parameters:** +- `value`: Base wait in milliseconds (`10`-`5000`) + +**Default:** `175` + +**Note:** The configured base is added to packet-length airtime and `direct.txdelay` jitter. Preset defaults are already reduced to account for the added `direct.txdelay` component. + +--- + +#### View or change the direct retry per-attempt add time (milliseconds) +**Usage:** +- `get direct.retry.step` +- `set direct.retry.step ` + +**Parameters:** +- `value`: Milliseconds added per retry attempt (`0`-`5000`) + +**Default:** `100` + +**Note:** This controls the linear add after the first retry wait. For example, `base=300` and `step=150` adds `0`, `150`, `300`, ... ms across retry attempts. --- diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index c7988bb344..362ba68a33 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -233,6 +233,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 + file.read((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122 file.close(); } @@ -273,6 +274,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 + file.write((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122 file.close(); } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index e8c1914bad..8a6f63c533 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -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; +} diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index aeff591cf4..ad0909bcca 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -165,7 +165,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { public: void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); } - + #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0"); @@ -177,6 +177,9 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { } #endif + // To check if there is pending work + bool hasPendingWork() const; + private: void writeOKFrame(); void writeErrFrame(uint8_t err_code); diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 48c381ceaf..ecb117bd2f 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -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]; -}; \ No newline at end of file +}; diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 876dc9c33c..bb350fd047 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -2,6 +2,11 @@ #include #include "MyMesh.h" +#ifdef ESP32_PLATFORM +#include "esp_pm.h" +#include "esp_bt.h" +#endif + // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -42,8 +47,8 @@ static uint32_t _atoi(const char* sp) { #define TCP_PORT 5000 #endif #elif defined(BLE_PIN_CODE) - #include - SerialBLEInterface serial_interface; + #include + SerialBLEInterface serial_interface; #elif defined(SERIAL_RX) #include ArduinoSerialInterface serial_interface; @@ -220,6 +225,33 @@ void setup() { #ifdef DISPLAY_CLASS ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved #endif + +#ifdef ESP32_PLATFORM + // Enable BLE sleep + esp_err_t errBLESleep = esp_bt_sleep_enable(); + if (errBLESleep == ESP_OK) { + Serial.println("Bluetooth sleep enabled successfully"); + } else { + Serial.printf("Bluetooth sleep enable failed: %s\n", esp_err_to_name(errBLESleep)); + } + +#if CONFIG_IDF_TARGET_ESP32C3 + esp_pm_config_esp32c3_t pm_config; +#elif CONFIG_IDF_TARGET_ESP32S3 + esp_pm_config_esp32s3_t pm_config; +#elif CONFIG_IDF_TARGET_ESP32 + esp_pm_config_esp32_t pm_config; +#endif + + // Configure Power Management + pm_config = { .max_freq_mhz = 80, .min_freq_mhz = 40, .light_sleep_enable = true }; + esp_err_t errPM = esp_pm_configure(&pm_config); + if (errPM == ESP_OK) { + Serial.println("Power Management configured successfully"); + } else { + Serial.printf("Power Management failed to configure: %d\r\n", errPM); + } +#endif } void loop() { @@ -229,4 +261,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 + } } diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 666f79fc5c..f9a961b9f4 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1,5 +1,6 @@ #include "MyMesh.h" #include +#include /* ------------------------------ Config -------------------------------- */ @@ -40,6 +41,9 @@ #ifndef TXT_ACK_DELAY #define TXT_ACK_DELAY 200 #endif +#ifndef HALO_DIRECT_RETRY_DELAY_MIN + #define HALO_DIRECT_RETRY_DELAY_MIN 200 +#endif #define FIRMWARE_VER_LEVEL 2 @@ -60,6 +64,103 @@ #define LAZY_CONTACTS_WRITE_DELAY 5000 +static void formatRecentRepeaterPrefix(const SimpleMeshTables::RecentRepeaterInfo* info, char* out, size_t out_len) { + if (out == NULL || out_len == 0) { + return; + } + out[0] = 0; + if (info == NULL) { + return; + } + + uint8_t prefix_len = info->prefix_len; + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + if (prefix_len > 0) { + mesh::Utils::toHex(out, info->prefix, prefix_len); + } + + size_t used = strlen(out); + const size_t target_len = MAX_ROUTE_HASH_BYTES * 2; + while (used < target_len && used + 1 < out_len) { + out[used++] = ' '; + } + out[used] = 0; +} + +static void formatRecentRepeaterSnrX4(int8_t snr_x4, char* out, size_t out_len) { + if (out == NULL || out_len == 0) { + return; + } + + const char* snr_text = StrHelper::ftoa(((float)snr_x4) / 4.0f); + if (snr_text[0] == '-') { + snprintf(out, out_len, "%s", snr_text); + } else { + snprintf(out, out_len, " %s", snr_text); + } +} + +static int buildSortedRecentRepeaterView(SimpleMeshTables* tables, + const SimpleMeshTables::RecentRepeaterInfo** out, + int out_cap) { + if (tables == NULL || out == NULL || out_cap <= 0) { + return 0; + } + + int total = tables->getRecentRepeaterCount(); + if (total > out_cap) { + total = out_cap; + } + + int count = 0; + for (int i = 0; i < total; i++) { + const auto* info = tables->getRecentRepeaterNewestByIdx(i); + if (info != NULL) { + out[count++] = info; + } + } + + std::stable_sort(out, out + count, [](const SimpleMeshTables::RecentRepeaterInfo* a, + const SimpleMeshTables::RecentRepeaterInfo* b) { + uint8_t a_len = a->prefix_len; + uint8_t b_len = b->prefix_len; + if (a_len > MAX_ROUTE_HASH_BYTES) a_len = MAX_ROUTE_HASH_BYTES; + if (b_len > MAX_ROUTE_HASH_BYTES) b_len = MAX_ROUTE_HASH_BYTES; + + if (a_len != b_len) { + return a_len > b_len; // 3-byte first, then 2-byte, then 1-byte + } + if (a->snr_x4 != b->snr_x4) { + return a->snr_x4 > b->snr_x4; // highest SNR first within each prefix size + } + return false; // keep original newest-first order for ties + }); + + return count; +} + +static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) { + uint8_t code = flags & 0x03; + uint8_t size_pow2 = (uint8_t)(1U << code); // legacy TRACE interpretation + uint8_t size_linear = (uint8_t)(code + 1U); // packed-size interpretation (1..4) + + bool pow2_ok = size_pow2 > 0 && (route_bytes % size_pow2) == 0; + bool linear_ok = size_linear > 0 && (route_bytes % size_linear) == 0; + + if (pow2_ok && !linear_ok) { + return size_pow2; + } + if (linear_ok && !pow2_ok) { + return size_linear; + } + if (pow2_ok) { + return size_pow2; + } + return size_linear; +} + void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { #if MAX_NEIGHBOURS // check if neighbours enabled // find existing neighbour, else use least recently updated @@ -399,6 +500,8 @@ File MyMesh::openAppend(const char *fname) { static uint8_t max_loop_minimal[] = { 0, /* 1-byte */ 4, /* 2-byte */ 2, /* 3-byte */ 1 }; static uint8_t max_loop_moderate[] = { 0, /* 1-byte */ 2, /* 2-byte */ 1, /* 3-byte */ 1 }; static uint8_t max_loop_strict[] = { 0, /* 1-byte */ 1, /* 2-byte */ 1, /* 3-byte */ 1 }; +// SF5..SF12 receive floors, scaled by 4 so we can keep the retry gate in int8_t quarter-dB units. +static const int8_t direct_retry_floor_x4[] = { -10, -20, -30, -40, -50, -60, -70, -80 }; bool MyMesh::isLooped(const mesh::Packet* packet, const uint8_t max_counters[]) { uint8_t hash_size = packet->getPathHashSize(); @@ -428,6 +531,30 @@ void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, ui bool MyMesh::allowPacketForward(const mesh::Packet *packet) { if (_prefs.disable_fwd) return false; + + if (packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE && packet->payload_len >= 9) { + auto* tables = (SimpleMeshTables *)getTables(); + uint8_t route_bytes = packet->payload_len - 9; + uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes); + uint16_t offset = (uint16_t)packet->path_len * (uint16_t)hash_size; + uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); + int16_t fallback_snr_x4 = direct_retry_floor_x4[sf - 5] + 40; // fixed +10 dB above SF floor + + // A successful TRACE forward reveals the downstream next-hop hash. Seed/update the recent table immediately. + if (hash_size > 0 && offset + (2U * hash_size) <= route_bytes) { + uint8_t prefix_len = hash_size; + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + const uint8_t* next_hop_prefix = &packet->payload[9 + offset + hash_size]; + const auto* existing = tables->findRecentRepeaterByHash(next_hop_prefix, prefix_len); + // This point only proves we can forward TO next_hop; packet->_snr is upstream RX and not a + // trustworthy metric for next_hop. Seed with existing table value or fallback only. + int8_t trace_snr_x4 = (existing != NULL) ? existing->snr_x4 : (int8_t)constrain(fallback_snr_x4, -128, 127); + tables->setRecentRepeater(next_hop_prefix, prefix_len, trace_snr_x4, false, true); + } + } + if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; if (packet->isRouteFlood() && recv_pkt_region == NULL) { MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet"); @@ -531,6 +658,127 @@ void MyMesh::logTxFail(mesh::Packet *pkt, int len) { } } +void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { + if (packet == NULL) { + return; + } + if (strcmp(event, "failure") == 0) { + return; + } + + uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; + uint8_t prefix_len = 0; + bool has_prefix = extractDirectRetryPrefix(packet, prefix, prefix_len); + auto* tables = (SimpleMeshTables *)getTables(); + const auto* existing = has_prefix ? tables->findRecentRepeaterByHash(prefix, prefix_len) : NULL; + char next_hop_hex[(MAX_ROUTE_HASH_BYTES * 2) + 1] = {0}; + if (has_prefix && prefix_len > 0) { + mesh::Utils::toHex(next_hop_hex, prefix, prefix_len); + } + const char* next_hop = (has_prefix && prefix_len > 0) ? next_hop_hex : "unknown"; + char hop_text[24]; + if (packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE && packet->payload_len >= 9) { + uint8_t route_bytes = packet->payload_len - 9; + uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes); + uint8_t total_hops = (hash_size > 0) ? (route_bytes / hash_size) : 0; + if (total_hops > 0) { + snprintf(hop_text, sizeof(hop_text), "%u/%u", (unsigned int)packet->path_len, (unsigned int)total_hops); + } else { + snprintf(hop_text, sizeof(hop_text), "unknown"); + } + } else if (packet->isRouteDirect()) { + snprintf(hop_text, sizeof(hop_text), "remaining:%u", (unsigned int)packet->getPathHashCount()); + } else { + snprintf(hop_text, sizeof(hop_text), "unknown"); + } + + // Direct-retry events are TX-side and usually have no trustworthy RX SNR. + // Cap event SNR at fixed SF floor + 10 dB so trace-start retries can't inflate table SNR. + uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); + int16_t fallback_snr_x4_raw = direct_retry_floor_x4[sf - 5] + 40; + int8_t fallback_snr_x4 = (int8_t)constrain(fallback_snr_x4_raw, -128, 127); + bool is_success_event = strcmp(event, "good") == 0; + bool updates_quality = strcmp(event, "good") == 0; + int8_t retry_event_snr_x4; + if (is_success_event) { + // On success, Mesh.cpp injects echo RX SNR for the downstream retry target. + retry_event_snr_x4 = packet->_snr; + } else if (existing != NULL) { + retry_event_snr_x4 = existing->snr_x4; + } else { + retry_event_snr_x4 = fallback_snr_x4; + } + char snr_pkt_text[12]; + char snr_table_text[12]; + + if (has_prefix && updates_quality) { + // Refresh SNR only once per successful echo/progress event, not on queued/resent bookkeeping. + tables->setRecentRepeater(prefix, prefix_len, retry_event_snr_x4, false, true); + } + + if (strcmp(event, "failed_all_tries") == 0) { + if (has_prefix) { + if (existing == NULL) { + int16_t seed_snr_x4 = (int16_t)getDirectRetryMinSNRX4() + 10; // +2.5 dB over the active retry cutoff. + tables->setRecentRepeater(prefix, prefix_len, (int8_t)constrain(seed_snr_x4, -128, 127), false, true); + } + // SNR is stored in quarter-dB units, so 1 lowers quality by 0.25 dB. + tables->decrementRecentRepeaterSnrX4(prefix, prefix_len, 1); + } + } + + snprintf(snr_pkt_text, sizeof(snr_pkt_text), "%s", StrHelper::ftoa(((float)packet->_snr) / 4.0f)); + const auto* log_existing = has_prefix ? tables->findRecentRepeaterByHash(prefix, prefix_len) : NULL; + if (log_existing != NULL) { + snprintf(snr_table_text, sizeof(snr_table_text), "%s", StrHelper::ftoa(((float)log_existing->snr_x4) / 4.0f)); + } else { + snprintf(snr_table_text, sizeof(snr_table_text), "na"); + } + + const char* time_label = "time_ms"; + if (strcmp(event, "queued") == 0 || strcmp(event, "dropped_queue_full") == 0) { + time_label = "wait_ms"; + } else if (strcmp(event, "resent") == 0 || strcmp(event, "failed_all_tries") == 0 || strcmp(event, "failure") == 0) { + time_label = "elapsed_ms"; + } else if (strcmp(event, "good") == 0) { + time_label = "echo_ms"; + } + + MESH_DEBUG_PRINTLN("%s direct retry %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%s, next_hop=%s, pkt=%s, tbl=%s, %s=%lu)", + getLogDateTime(), + event, + (unsigned int)retry_attempt, + (uint32_t)packet->getPayloadType(), + packet->isRouteDirect() ? "D" : "F", + (uint32_t)packet->payload_len, + hop_text, + next_hop, + snr_pkt_text, + snr_table_text, + time_label, + (unsigned long)delay_millis); + + if (_logging) { + File f = openAppend(PACKET_LOG_FILE); + if (f) { + f.print(getLogDateTime()); + f.printf(": DIRECT RETRY %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%s, next_hop=%s, pkt=%s, tbl=%s, %s=%lu)\n", + event, + (unsigned int)retry_attempt, + (uint32_t)packet->getPayloadType(), + packet->isRouteDirect() ? "D" : "F", + (uint32_t)packet->payload_len, + hop_text, + next_hop, + snr_pkt_text, + snr_table_text, + time_label, + (unsigned long)delay_millis); + f.close(); + } + } +} + int MyMesh::calcRxDelay(float score, uint32_t air_time) const { if (_prefs.rx_delay_base <= 0.0f) return 0; return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); @@ -544,6 +792,121 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } +int8_t MyMesh::getDirectRetryMinSNRX4() const { + // Use the live SF so `tempradio` changes immediately affect the retry threshold. + uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); + int16_t margin_x4 = (int16_t)_prefs.direct_retry_snr_margin_db; + int16_t threshold = direct_retry_floor_x4[sf - 5] + margin_x4; + return (int8_t)constrain(threshold, -128, 127); +} +bool MyMesh::extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { + if (packet == NULL || prefix == NULL) { + return false; + } + + // TRACE direct routes encode repeater hashes in payload; packet->path carries SNR trail bytes. + if (packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE && packet->payload_len >= 9) { + uint8_t route_bytes = packet->payload_len - 9; + uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes); + uint8_t offset = packet->path_len * hash_size; + if (hash_size > 0 && offset + hash_size <= route_bytes) { + prefix_len = hash_size; + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + memcpy(prefix, &packet->payload[9 + offset], prefix_len); + return true; + } + } + + if (packet->isRouteDirect() && packet->getPathHashCount() > 0) { + prefix_len = packet->getPathHashSize(); + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + if (prefix_len == 0) { + return false; + } + memcpy(prefix, packet->path, prefix_len); + return true; + } + + return false; +} +bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { + if (_prefs.disable_fwd) { + return false; + } + + int8_t min_snr_x4 = getDirectRetryMinSNRX4(); + if (_prefs.direct_retry_recent_enabled) { + const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len); + return recent == NULL || recent->snr_x4 >= min_snr_x4; + } + + return true; +} +uint8_t MyMesh::getDirectRetryPreset() const { + if (_prefs.direct_retry_preset <= DIRECT_RETRY_PRESET_MOBILE) { + return _prefs.direct_retry_preset; + } + return DIRECT_RETRY_PRESET_ROOFTOP; +} +uint8_t MyMesh::getDirectRetryConfiguredMaxAttempts() const { + return constrain(_prefs.direct_retry_attempts, (uint8_t)1, (uint8_t)15); +} +uint32_t MyMesh::getDirectRetryAttemptStepMillis() const { + return constrain((uint32_t)_prefs.direct_retry_step_ms, (uint32_t)0, (uint32_t)5000); +} +uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { + uint32_t base_wait_millis = constrain((uint32_t)_prefs.direct_retry_base_ms, (uint32_t)10, (uint32_t)5000); + if (packet == NULL) { + return base_wait_millis; + } + + // Approximate LoRa line rate in kilobits/sec from the live radio params the repeater is using now. + float kbps = (((float) active_sf) * active_bw * ((float) active_cr)) / ((float) (1UL << active_sf)); + if (kbps <= 0.0f) { + return base_wait_millis; + } + + // Wait roughly long enough for our transmission, the next hop's receive/forward window, and its echo back. + uint32_t bits = ((uint32_t) packet->getRawLength()) * 8; + uint32_t scaled_wait_millis = (uint32_t) ((((float) bits) * 4.0f) / kbps); + return base_wait_millis + scaled_wait_millis; +} +uint8_t MyMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const { + uint8_t configured_attempts = getDirectRetryConfiguredMaxAttempts(); + uint8_t total_hops = 0; + + if (packet != NULL) { + if (packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE && packet->payload_len >= 9) { + uint8_t route_bytes = packet->payload_len - 9; + uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes); + if (hash_size > 0) { + total_hops = (uint8_t)(route_bytes / hash_size); + } + } else { + total_hops = packet->getPathHashCount(); + } + } + + uint8_t path_cap = 15; + if (total_hops <= 3) { + path_cap = 8; + } else if (total_hops == 4) { + path_cap = 12; + } + + return configured_attempts < path_cap ? configured_attempts : path_cap; +} +uint32_t MyMesh::getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) { + uint32_t retry_delay = getDirectRetryEchoDelay(packet) + ((uint32_t)attempt_idx * getDirectRetryAttemptStepMillis()); + if (packet == NULL) { + return retry_delay; + } + return getDirectRetransmitDelay(packet) + retry_delay; +} bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { // just try to determine region for packet (apply later in allowPacketForward()) @@ -874,6 +1237,12 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.tx_delay_factor = 0.5f; // was 0.25f _prefs.direct_tx_delay_factor = 0.3f; // was 0.2 + _prefs.direct_retry_recent_enabled = 1; + _prefs.direct_retry_snr_margin_db = DIRECT_RETRY_ROOFTOP_MARGIN_X4; + _prefs.direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT; + _prefs.direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS; + _prefs.direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS; + _prefs.direct_retry_preset = DIRECT_RETRY_PRESET_ROOFTOP; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; _prefs.node_lon = ADVERT_LON; @@ -911,11 +1280,13 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.rx_boosted_gain = 1; // enabled by default; #endif #endif + _prefs.radio_fem_rxgain = 1; pending_discover_tag = 0; pending_discover_until = 0; - - memset(default_scope.key, 0, sizeof(default_scope.key)); + active_bw = _prefs.bw; + active_sf = _prefs.sf; + active_cr = _prefs.cr; } void MyMesh::begin(FILESYSTEM *fs) { @@ -954,11 +1325,16 @@ void MyMesh::begin(FILESYSTEM *fs) { #endif radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + active_bw = _prefs.bw; + active_sf = _prefs.sf; + active_cr = _prefs.cr; + ((SimpleMeshTables *)getTables())->setRecentRepeaterMinSNRX4(getDirectRetryMinSNRX4()); radio_set_tx_power(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); + board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); updateAdvertTimer(); updateFloodAdvertTimer(); @@ -1242,6 +1618,186 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply Serial.printf("\n"); } reply[0] = 0; + } else if (strncmp(command, "get recent.repeater", 19) == 0 + || strncmp(command, "set recent.repeater", 19) == 0 + || strncmp(command, "clear recent.repeater", 21) == 0 + || strncmp(command, "recent.repeater", 15) == 0) { + bool is_get = false; + bool is_set = false; + bool is_clear = false; + const char* sub = command; + + if (strncmp(command, "get recent.repeater", 19) == 0) { + is_get = true; + sub = command + 19; + } else if (strncmp(command, "set recent.repeater", 19) == 0) { + is_set = true; + sub = command + 19; + } else if (strncmp(command, "clear recent.repeater", 21) == 0) { + is_clear = true; + sub = command + 21; + } else { + sub = command + 15; // legacy command format + } + while (*sub == ' ') sub++; + + auto* tables = (SimpleMeshTables*)getTables(); + if (!is_get && !is_set && !is_clear && strncmp(sub, "clear", 5) == 0 && (sub[5] == 0 || sub[5] == ' ')) { + is_clear = true; + sub += 5; + while (*sub == ' ') sub++; + } + + if (is_clear) { + if (*sub != 0) { + strcpy(reply, "Err - usage: clear recent.repeater"); + } else { + tables->clearRecentRepeaters(); + strcpy(reply, "OK"); + } + } else if (is_set) { + char* params = (char*) sub; + char* arg_snr = strchr(params, ' '); + if (arg_snr == NULL) { + strcpy(reply, "Err - usage: set recent.repeater "); + } else { + *arg_snr++ = 0; + while (*arg_snr == ' ') arg_snr++; + if (*arg_snr == 0) { + strcpy(reply, "Err - usage: set recent.repeater "); + } else { + uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; + int hex_len = strlen(params); + if (hex_len != (MAX_ROUTE_HASH_BYTES * 2) || !mesh::Utils::fromHex(prefix, MAX_ROUTE_HASH_BYTES, params)) { + strcpy(reply, "Err - prefix must be exactly 3 bytes hex (6 chars)"); + } else { + char* end_snr = NULL; + float snr_db = strtof(arg_snr, &end_snr); + while (end_snr != NULL && *end_snr == ' ') end_snr++; + if (end_snr == arg_snr || (end_snr != NULL && *end_snr != 0)) { + strcpy(reply, "Err - snr must be numeric"); + return; + } + + int snr_x4 = (int)(snr_db * 4.0f + (snr_db >= 0.0f ? 0.5f : -0.5f)); + snr_x4 = constrain(snr_x4, -128, 127); + if (tables->setRecentRepeater(prefix, MAX_ROUTE_HASH_BYTES, (int8_t)snr_x4, true)) { + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - unable to store prefix"); + } + } + } + } + } else { + const long page_size = sender_timestamp == 0 ? 128 : 7; + long page_num = 1; + const char* arg = sub; + + if (strncmp(arg, "page ", 5) == 0) { + arg += 5; + while (*arg == ' ') arg++; + } + + if (*arg != 0) { + char* end_ptr = NULL; + page_num = strtol(arg, &end_ptr, 10); + while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; + if (end_ptr == NULL || page_num <= 0 || (end_ptr != NULL && *end_ptr != 0)) { + strcpy(reply, "Err - usage: get recent.repeater [page]"); + return; + } + } + + size_t sorted_size = sizeof(SimpleMeshTables::RecentRepeaterInfo*) * MAX_RECENT_REPEATERS; + const SimpleMeshTables::RecentRepeaterInfo** sorted_recent = + (const SimpleMeshTables::RecentRepeaterInfo**)malloc(sorted_size); + if (sorted_recent == NULL) { + strcpy(reply, "Err - unable to allocate recent repeater view"); + return; + } + + int total = buildSortedRecentRepeaterView(tables, sorted_recent, MAX_RECENT_REPEATERS); + if (total <= 0) { + strcpy(reply, "> none"); + } else { + int total_pages = (total + (int)page_size - 1) / (int)page_size; + if (page_num > total_pages) { + sprintf(reply, "> none (page=%ld/%d)", page_num, total_pages); + free(sorted_recent); + return; + } + + int offset = ((int)page_num - 1) * (int)page_size; + int limit = total - offset; + if (limit > (int)page_size) { + limit = (int)page_size; + } + + if (sender_timestamp == 0) { + Serial.printf("Recent repeater table (3-byte,2-byte,1-byte; SNR desc, page=%ld/%d, n=%d/%d):\n", + page_num, + total_pages, + limit, + total); + for (int i = 0; i < limit; i++) { + const auto* info = sorted_recent[offset + i]; + if (info == NULL) { + continue; + } + + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; + formatRecentRepeaterPrefix(info, hex, sizeof(hex)); + char snr_text[12]; + formatRecentRepeaterSnrX4(info->snr_x4, snr_text, sizeof(snr_text)); + Serial.printf("%s,%s%s\n", + hex, + snr_text, + info->snr_locked ? ",l" : ""); + } + sprintf(reply, "> page=%ld/%d n=%d/%d", page_num, total_pages, limit, total); + } else { + int written = snprintf(reply, 160, "> page=%ld/%d n=%d/%d", page_num, total_pages, limit, total); + bool truncated = false; + if (written < 0) { + reply[0] = 0; + written = 0; + } + + for (int i = 0; i < limit; i++) { + int idx = offset + i; + const auto* info = sorted_recent[idx]; + if (info == NULL) { + continue; + } + if (written >= 154) { + truncated = true; + break; + } + + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; + formatRecentRepeaterPrefix(info, hex, sizeof(hex)); + char snr_text[12]; + formatRecentRepeaterSnrX4(info->snr_x4, snr_text, sizeof(snr_text)); + int n = snprintf(reply + written, + 160 - written, + "\n%s,%s%s", + hex, + snr_text, + info->snr_locked ? ",l" : ""); + if (n < 0 || n >= (160 - written)) { + truncated = true; + break; + } + written += n; + } + if (truncated && written < 156) { + snprintf(reply + written, 160 - written, "\n... next page"); + } + } + } + free(sorted_recent); + } } else if (memcmp(command, "discover.neighbors", 18) == 0) { const char* sub = command + 18; while (*sub == ' ') sub++; @@ -1280,15 +1836,24 @@ void MyMesh::loop() { if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params set_radio_at = 0; // clear timer radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr); + active_bw = pending_bw; + active_sf = pending_sf; + active_cr = pending_cr; MESH_DEBUG_PRINTLN("Temp radio params"); } if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig revert_radio_at = 0; // clear timer radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + active_bw = _prefs.bw; + active_sf = _prefs.sf; + active_cr = _prefs.cr; MESH_DEBUG_PRINTLN("Radio params restored"); } + // Keep recent-prefix learning aligned with the live retry SNR gate. + ((SimpleMeshTables *)getTables())->setRecentRepeaterMinSNRX4(getDirectRetryMinSNRX4()); + // is pending dirty contacts write needed? if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { acl.save(_fs); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 8ed0317e69..1a2f505c5e 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -110,8 +110,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { unsigned long set_radio_at, revert_radio_at; float pending_freq; float pending_bw; + float active_bw; // live BW, including temporary radio overrides uint8_t pending_sf; + uint8_t active_sf; // live SF, including temporary radio overrides uint8_t pending_cr; + uint8_t active_cr; // live CR, including temporary radio overrides int matching_peer_indexes[MAX_CLIENTS]; #if defined(WITH_RS232_BRIDGE) RS232Bridge bridge; @@ -119,6 +122,11 @@ 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 getDirectRetryPreset() const; + uint8_t getDirectRetryConfiguredMaxAttempts() const; + uint32_t getDirectRetryAttemptStepMillis() const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); @@ -146,6 +154,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t getRetransmitDelay(const mesh::Packet* packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; + bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override; + uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override; + 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; int getInterferenceThreshold() const override { return _prefs.interference_threshold; diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index acb4632581..ac6958bae8 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -41,7 +41,8 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi } // v1.2.3 (1 Jan 2025) - sprintf(_version_info, "%s (%s)", version, build_date); + snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date); + free(version); } void UITask::renderCurrScreen() { diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index e37078ce5f..17a3c1923d 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -20,8 +20,7 @@ void halt() { static char command[160]; // For power saving -unsigned long lastActive = 0; // mark last active time -unsigned long nextSleepinSecs = 120; // next sleep in seconds. The first sleep (if enabled) is after 2 minutes from boot +unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot #if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_) static unsigned long userBtnDownAt = 0; @@ -40,9 +39,6 @@ void setup() { delay(5000); #endif - // For power saving - lastActive = millis(); // mark last active time since boot - #ifdef DISPLAY_CLASS if (display.begin()) { display.startFrame(); @@ -155,16 +151,12 @@ void loop() { rtc_clock.tick(); if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { - #if defined(NRF52_PLATFORM) +#if defined(NRF52_PLATFORM) board.sleep(1800); // nrf ignores seconds param, sleeps whenever possible - #else - if (the_mesh.millisHasNowPassed(lastActive + nextSleepinSecs * 1000)) { // To check if it is time to sleep - board.sleep(1800); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet - lastActive = millis(); - nextSleepinSecs = 5; // Default: To work for 5s and sleep again - } else { - nextSleepinSecs += 5; // When there is pending work, to work another 5s +#else + if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep + board.sleep(30); // Sleep. Wake up after some seconds or when receiving a LoRa packet } - #endif +#endif } } diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 145fb0fd9f..bdda819453 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -652,6 +652,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.gps_enabled = 0; _prefs.gps_interval = 0; _prefs.advert_loc_policy = ADVERT_LOC_PREFS; + _prefs.radio_fem_rxgain = 1; next_post_idx = 0; next_client_idx = 0; @@ -693,6 +694,7 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_set_tx_power(_prefs.tx_power_dbm); + board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); updateAdvertTimer(); updateFloodAdvertTimer(); @@ -1022,3 +1024,11 @@ void MyMesh::loop() { uptime_millis += now - last_millis; last_millis = now; } + +// To check if there is pending work +bool MyMesh::hasPendingWork() const { +#if defined(WITH_BRIDGE) + if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep +#endif + return _mgr->getOutboundTotal() > 0; +} diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 1b35ae95a1..410ffef019 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -222,4 +222,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void clearStats() override; void handleCommand(uint32_t sender_timestamp, char* command, char* reply); void loop(); + + // To check if there is pending work + bool hasPendingWork() const; }; diff --git a/examples/simple_room_server/UITask.cpp b/examples/simple_room_server/UITask.cpp index 42bc14d4a5..6445baf6f7 100644 --- a/examples/simple_room_server/UITask.cpp +++ b/examples/simple_room_server/UITask.cpp @@ -41,7 +41,8 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi } // v1.2.3 (1 Jan 2025) - sprintf(_version_info, "%s (%s)", version, build_date); + snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date); + free(version); } void UITask::renderCurrScreen() { diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 825fb007d5..5517980936 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -18,6 +18,9 @@ void halt() { static char command[MAX_POST_TEXT_LEN+1]; +// For power saving +unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot + void setup() { Serial.begin(115200); delay(1000); @@ -113,4 +116,14 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + + if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { +#if defined(NRF52_PLATFORM) + board.sleep(1800); // nrf ignores seconds param, sleeps whenever possible +#else + if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep + board.sleep(1800); // Sleep. Wake up after 30 minutes or when receiving a LoRa packet + } +#endif + } } diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index b8fe1e579c..05b4a9ddb5 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -731,6 +731,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise _prefs.gps_enabled = 0; _prefs.gps_interval = 0; _prefs.advert_loc_policy = ADVERT_LOC_PREFS; + _prefs.radio_fem_rxgain = 1; memset(default_scope.key, 0, sizeof(default_scope.key)); } @@ -766,6 +767,7 @@ void SensorMesh::begin(FILESYSTEM* fs) { radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_set_tx_power(_prefs.tx_power_dbm); + board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/examples/simple_sensor/UITask.cpp b/examples/simple_sensor/UITask.cpp index 0e78fee005..698b9fd2f2 100644 --- a/examples/simple_sensor/UITask.cpp +++ b/examples/simple_sensor/UITask.cpp @@ -41,7 +41,8 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi } // v1.2.3 (1 Jan 2025) - sprintf(_version_info, "%s (%s)", version, build_date); + snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date); + free(version); } void UITask::renderCurrScreen() { diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 9d7a11131d..cccbd36c79 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -106,6 +106,7 @@ void Dispatcher::loop() { _radio->onSendFinished(); logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); + onSendComplete(outbound); if (outbound->isRouteFlood()) { n_sent_flood++; } else { @@ -118,6 +119,7 @@ void Dispatcher::loop() { _radio->onSendFinished(); logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); + onSendFail(outbound); releasePacket(outbound); // return to pool outbound = NULL; @@ -386,4 +388,4 @@ unsigned long Dispatcher::futureMillis(int millis_from_now) const { return _ms->getMillis() + millis_from_now; } -} \ No newline at end of file +} diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 2a99d0682b..90ee5cdbea 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -159,6 +159,8 @@ class Dispatcher { virtual void logRx(Packet* packet, int len, float score) { } // hooks for custom logging virtual void logTx(Packet* packet, int len) { } virtual void logTxFail(Packet* packet, int len) { } + virtual void onSendComplete(Packet* packet) { } + virtual void onSendFail(Packet* packet) { } virtual const char* getLogDateTime() { return ""; } virtual float getAirtimeBudgetFactor() const; @@ -168,6 +170,7 @@ class Dispatcher { virtual int getInterferenceThreshold() const { return 0; } // disabled by default virtual int getAGCResetInterval() const { return 0; } // disabled by default virtual unsigned long getDutyCycleWindowMs() const { return 3600000; } + const Packet* getOutboundInFlight() const { return outbound; } public: void begin(); diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 57fee14036..42bcf5b10e 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -3,12 +3,81 @@ namespace mesh { +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 15; +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; + +static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) { + uint8_t code = flags & 0x03; + uint8_t size_pow2 = (uint8_t)(1U << code); // legacy TRACE interpretation + uint8_t size_linear = (uint8_t)(code + 1U); // packed-size interpretation (1..4) + + bool pow2_ok = size_pow2 > 0 && (route_bytes % size_pow2) == 0; + bool linear_ok = size_linear > 0 && (route_bytes % size_linear) == 0; + + if (pow2_ok && !linear_ok) { + return size_pow2; + } + if (linear_ok && !pow2_ok) { + return size_linear; + } + if (pow2_ok) { + return size_pow2; + } + return size_linear; +} + void Mesh::begin() { + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + _direct_retries[i].packet = NULL; + _direct_retries[i].trigger_packet = NULL; + _direct_retries[i].retry_started_at = 0; + _direct_retries[i].echo_wait_started_at = 0; + _direct_retries[i].retry_at = 0; + _direct_retries[i].retry_delay = 0; + _direct_retries[i].retry_attempts_sent = 0; + _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; + } 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); + } + } } bool Mesh::allowPacketForward(const mesh::Packet* packet) { @@ -22,10 +91,33 @@ uint32_t Mesh::getRetransmitDelay(const mesh::Packet* packet) { uint32_t Mesh::getDirectRetransmitDelay(const Packet* packet) { return 0; // by default, no delay } +bool Mesh::allowDirectRetry(const Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { + return false; +} +uint32_t Mesh::getDirectRetryEchoDelay(const Packet* packet) const { + // Keep the base fallback aligned with the repeater's minimum retry wait. + return 200; +} +uint8_t Mesh::getDirectRetryMaxAttempts(const Packet* packet) const { + return DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT; +} +uint32_t Mesh::getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) { + uint32_t base = getDirectRetryEchoDelay(packet); + // Keep the historical linear spacing while allowing the base wait to vary by platform/profile. + return base + ((uint32_t)attempt_idx * 100UL); +} uint8_t Mesh::getExtraAckTransmitCount() const { return 0; } +void Mesh::onSendComplete(Packet* packet) { + armDirectRetryOnSendComplete(packet); +} + +void Mesh::onSendFail(Packet* packet) { + clearPendingDirectRetryOnSendFail(packet); +} + uint32_t Mesh::getCADFailRetryDelay() const { return _rng->nextInt(1, 4)*120; } @@ -39,6 +131,10 @@ int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int } DispatcherAction Mesh::onRecvPacket(Packet* pkt) { + if (pkt->isRouteDirect()) { + cancelDirectRetryOnEcho(pkt); + } + if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { if (pkt->path_len < MAX_PATH_SIZE) { uint8_t i = 0; @@ -47,17 +143,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 +196,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { removeSelfFromPath(pkt); uint32_t d = getDirectRetransmitDelay(pkt); + maybeScheduleDirectRetry(pkt, 0); return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority } } @@ -372,6 +471,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 +482,324 @@ 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; + uint32_t retry_delay = getDirectRetryAttemptDelay(packet, _direct_retries[i].retry_attempts_sent); + sendPacket(retry, _direct_retries[i].priority, retry_delay); + if (isDirectRetryQueued(retry)) { + _direct_retries[i].packet = retry; + _direct_retries[i].retry_delay = retry_delay; + _direct_retries[i].retry_at = futureMillis(retry_delay); + _direct_retries[i].waiting_final_echo = false; + onDirectRetryEvent("queued", retry, retry_delay, _direct_retries[i].retry_attempts_sent + 1); + } else { + onDirectRetryEvent("dropped_queue_full", retry, retry_delay, _direct_retries[i].retry_attempts_sent + 1); + onDirectRetryEvent("failure", retry, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1); + 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; + + // Start the echo wait only after the initial direct transmission actually completed. + sendPacket(retry, _direct_retries[i].priority, _direct_retries[i].retry_delay); + if (isDirectRetryQueued(retry)) { + unsigned long now = _ms->getMillis(); + _direct_retries[i].packet = retry; + _direct_retries[i].trigger_packet = NULL; + _direct_retries[i].queued = true; + _direct_retries[i].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); + 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); + _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; +} + 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 +1047,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 +1076,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 +1109,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 +1120,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 +1133,9 @@ void Mesh::sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay packet->path_len = 0; // path_len of zero means Zero Hop - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us sendPacket(packet, 0, delay_millis); } -} \ No newline at end of file +} diff --git a/src/Mesh.h b/src/Mesh.h index f9f8786320..91aec9a669 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -4,6 +4,10 @@ namespace mesh { +#ifndef MAX_DIRECT_RETRY_SLOTS + #define MAX_DIRECT_RETRY_SLOTS 6 +#endif + class GroupChannel { public: uint8_t hash[PATH_HASH_SIZE]; @@ -16,6 +20,7 @@ class GroupChannel { class MeshTables { public: virtual bool hasSeen(const Packet* packet) = 0; + virtual void markSent(const Packet* packet) = 0; virtual void clear(const Packet* packet) = 0; // remove this packet hash from table }; @@ -24,17 +29,46 @@ class MeshTables { * and provides virtual methods for sub-classes on handling incoming, and also preparing outbound Packets. */ class Mesh : public Dispatcher { + struct DirectRetryEntry { + Packet* packet; + Packet* trigger_packet; + unsigned long retry_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; + }; + RTCClock* _rtc; RNG* _rng; MeshTables* _tables; + DirectRetryEntry _direct_retries[MAX_DIRECT_RETRY_SLOTS]; void removeSelfFromPath(Packet* packet); void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis); + void clearDirectRetrySlot(int idx); + bool isDirectRetryQueued(const Packet* packet) const; + void calculateDirectRetryKey(const Packet* packet, uint8_t* dest_key) const; + bool cancelDirectRetryOnEcho(const Packet* packet); + void armDirectRetryOnSendComplete(const Packet* packet); + void clearPendingDirectRetryOnSendFail(const Packet* packet); + bool getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len, + uint8_t& progress_marker, bool& expect_path_growth) const; + void maybeScheduleDirectRetry(const Packet* packet, uint8_t priority); //void routeRecvAcks(Packet* packet, uint32_t delay_millis); DispatcherAction forwardMultipartDirect(Packet* pkt); protected: DispatcherAction onRecvPacket(Packet* pkt) override; + void onSendComplete(Packet* packet) override; + void onSendFail(Packet* packet) override; virtual uint32_t getCADFailRetryDelay() const override; @@ -65,11 +99,37 @@ class Mesh : public Dispatcher { */ virtual uint32_t getDirectRetransmitDelay(const Packet* packet); + /** + * \brief Decide whether a DIRECT packet should get one delayed retry if the next hop echo is not overheard. + * Sub-classes can use neighbour tables or other link-quality data to opt in selectively. + */ + virtual bool allowDirectRetry(const Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const; + + /** + * \returns milliseconds to wait for the next-hop echo before queueing one retry of the DIRECT packet. + */ + virtual uint32_t getDirectRetryEchoDelay(const Packet* packet) const; + + /** + * \returns 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); + /** * \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 Perform search of local DB of peers/contacts. * \returns Number of peers with matching hash diff --git a/src/MeshCore.h b/src/MeshCore.h index 2db1d4c3ec..a79bd8b024 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -52,12 +52,16 @@ class MainBoard { virtual void onAfterTransmit() { } virtual void reboot() = 0; virtual void powerOff() { /* no op */ } + virtual uint32_t getIRQGpio() { return -1; } // not supported. Returns DIO1 (SX1262) and DIO0 (SX127x) virtual void sleep(uint32_t secs) { /* no op */ } virtual uint32_t getGpio() { return 0; } virtual void setGpio(uint32_t values) {} virtual uint8_t getStartupReason() const = 0; virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; } virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported + virtual bool setLoRaFemLnaEnabled(bool enable) { return false; } + virtual bool canControlLoRaFemLna() const { return false; } + virtual bool isLoRaFemLnaEnabled() const { return false; } // Power management interface (boards with power management override these) virtual bool isExternalPowered() { return false; } diff --git a/src/helpers/BaseSerialInterface.h b/src/helpers/BaseSerialInterface.h index e60927654b..ddde483091 100644 --- a/src/helpers/BaseSerialInterface.h +++ b/src/helpers/BaseSerialInterface.h @@ -15,6 +15,7 @@ class BaseSerialInterface { virtual bool isConnected() const = 0; + virtual bool isReadBusy() const = 0; virtual bool isWriteBusy() const = 0; virtual size_t writeFrame(const uint8_t src[], size_t len) = 0; virtual size_t checkRecvFrame(uint8_t dest[]) = 0; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index d495aada5f..dc206516f1 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -3,11 +3,34 @@ #include "TxtDataHelpers.h" #include "AdvertDataHelpers.h" #include +#include +#define STR_HELPER(x) #x +#define STR(x) STR_HELPER(x) #ifndef BRIDGE_MAX_BAUD #define BRIDGE_MAX_BAUD 115200 #endif +// These bytes used to be reserved/unused in persisted prefs, so keep a marker before trusting them. +#define DIRECT_RETRY_PREFS_MAGIC_0 0xD4 +#define DIRECT_RETRY_PREFS_MAGIC_1 0x52 +#define DIRECT_RETRY_RECENT_DEFAULT 1 +#define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT_X4 DIRECT_RETRY_ROOFTOP_MARGIN_X4 +#define DIRECT_RETRY_SNR_MARGIN_DB_MAX 40 +#define DIRECT_RETRY_SNR_MARGIN_X4_MAX (DIRECT_RETRY_SNR_MARGIN_DB_MAX * 4) +#define DIRECT_RETRY_TIMING_MAGIC_0 0xD5 +#define DIRECT_RETRY_TIMING_MAGIC_1 0x54 +#define DIRECT_RETRY_COUNT_DEFAULT DIRECT_RETRY_ROOFTOP_COUNT +#define DIRECT_RETRY_COUNT_MIN 1 +#define DIRECT_RETRY_COUNT_MAX 15 +#define DIRECT_RETRY_BASE_MS_DEFAULT DIRECT_RETRY_ROOFTOP_BASE_MS +#define DIRECT_RETRY_BASE_MS_MIN 10 +#define DIRECT_RETRY_BASE_MS_MAX 5000 +#define DIRECT_RETRY_STEP_MS_DEFAULT DIRECT_RETRY_ROOFTOP_STEP_MS +#define DIRECT_RETRY_STEP_MS_MIN 0 +#define DIRECT_RETRY_STEP_MS_MAX 5000 +#define DIRECT_RETRY_PRESET_DEFAULT DIRECT_RETRY_PRESET_ROOFTOP + // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -18,6 +41,107 @@ static uint32_t _atoi(const char* sp) { return n; } +static uint8_t directRetryMarginDbToX4(float margin_db) { + int32_t scaled_x4 = (int32_t)((margin_db * 4.0f) + 0.5f); // nearest 0.25 dB + return (uint8_t)constrain(scaled_x4, 0, DIRECT_RETRY_SNR_MARGIN_X4_MAX); +} + +static float directRetryMarginX4ToDb(uint8_t margin_x4) { + return ((float)margin_x4) / 4.0f; +} + +static uint8_t directRetryPresetOrDefault(uint8_t preset) { + if (preset <= DIRECT_RETRY_PRESET_MOBILE) { + return preset; + } + return DIRECT_RETRY_PRESET_DEFAULT; +} + +static const char* directRetryPresetName(uint8_t preset) { + switch (directRetryPresetOrDefault(preset)) { + case DIRECT_RETRY_PRESET_INFRA: + return "infra"; + case DIRECT_RETRY_PRESET_MOBILE: + return "mobile"; + case DIRECT_RETRY_PRESET_ROOFTOP: + default: + return "rooftop"; + } +} + +static uint8_t directRetryEffectiveCount(const NodePrefs* prefs) { + return constrain(prefs->direct_retry_attempts, (uint8_t)DIRECT_RETRY_COUNT_MIN, (uint8_t)DIRECT_RETRY_COUNT_MAX); +} + +static uint16_t directRetryEffectiveBaseMs(const NodePrefs* prefs) { + return constrain(prefs->direct_retry_base_ms, (uint16_t)DIRECT_RETRY_BASE_MS_MIN, (uint16_t)DIRECT_RETRY_BASE_MS_MAX); +} + +static uint16_t directRetryEffectiveStepMs(const NodePrefs* prefs) { + return constrain(prefs->direct_retry_step_ms, (uint16_t)DIRECT_RETRY_STEP_MS_MIN, (uint16_t)DIRECT_RETRY_STEP_MS_MAX); +} + +static uint8_t directRetryEffectiveMarginX4(const NodePrefs* prefs) { + return constrain(prefs->direct_retry_snr_margin_db, (uint8_t)0, (uint8_t)DIRECT_RETRY_SNR_MARGIN_X4_MAX); +} + +static uint16_t directRetryPresetStepDefault(uint8_t preset) { + switch (directRetryPresetOrDefault(preset)) { + case DIRECT_RETRY_PRESET_INFRA: + return DIRECT_RETRY_INFRA_STEP_MS; + case DIRECT_RETRY_PRESET_MOBILE: + return DIRECT_RETRY_MOBILE_STEP_MS; + case DIRECT_RETRY_PRESET_ROOFTOP: + default: + return DIRECT_RETRY_ROOFTOP_STEP_MS; + } +} + +static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) { + prefs->direct_retry_preset = directRetryPresetOrDefault(preset); + switch (prefs->direct_retry_preset) { + case DIRECT_RETRY_PRESET_INFRA: + prefs->direct_retry_base_ms = DIRECT_RETRY_INFRA_BASE_MS; + prefs->direct_retry_attempts = DIRECT_RETRY_INFRA_COUNT; + prefs->direct_retry_step_ms = DIRECT_RETRY_INFRA_STEP_MS; + prefs->direct_retry_snr_margin_db = DIRECT_RETRY_INFRA_MARGIN_X4; + break; + case DIRECT_RETRY_PRESET_MOBILE: + prefs->direct_retry_base_ms = DIRECT_RETRY_MOBILE_BASE_MS; + prefs->direct_retry_attempts = DIRECT_RETRY_MOBILE_COUNT; + prefs->direct_retry_step_ms = DIRECT_RETRY_MOBILE_STEP_MS; + prefs->direct_retry_snr_margin_db = DIRECT_RETRY_MOBILE_MARGIN_X4; + break; + case DIRECT_RETRY_PRESET_ROOFTOP: + default: + prefs->direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS; + prefs->direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT; + prefs->direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS; + prefs->direct_retry_snr_margin_db = DIRECT_RETRY_ROOFTOP_MARGIN_X4; + break; + } +} + +static bool parseDirectRetryPreset(const char* value, uint8_t& preset) { + if (value == NULL) { + return false; + } + if (strcmp(value, "0") == 0 || strcmp(value, "infra") == 0 + || strcmp(value, "infa") == 0 || strcmp(value, "infrastructure") == 0) { + preset = DIRECT_RETRY_PRESET_INFRA; + return true; + } + if (strcmp(value, "1") == 0 || strcmp(value, "rooftop") == 0) { + preset = DIRECT_RETRY_PRESET_ROOFTOP; + return true; + } + if (strcmp(value, "2") == 0 || strcmp(value, "mobile") == 0) { + preset = DIRECT_RETRY_PRESET_MOBILE; + return true; + } + return false; +} + static bool isValidName(const char *n) { while (*n) { if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false; @@ -60,7 +184,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84 file.read((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88 file.read((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104 - file.read(pad, 4); // 108 : 4 bytes unused + file.read((uint8_t *)&_prefs->direct_retry_recent_enabled, sizeof(_prefs->direct_retry_recent_enabled)); // 108 + file.read((uint8_t *)&_prefs->direct_retry_snr_margin_db, sizeof(_prefs->direct_retry_snr_margin_db)); // 109 + file.read((uint8_t *)&_prefs->direct_retry_prefs_magic[0], sizeof(_prefs->direct_retry_prefs_magic)); // 110 file.read((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112 file.read((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113 file.read((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 @@ -88,7 +214,27 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + uint8_t legacy_retry_attempts_or_radio_fem_rxgain = _prefs->direct_retry_attempts; + size_t legacy_retry_attempts_read = file.read((uint8_t *)&legacy_retry_attempts_or_radio_fem_rxgain, + sizeof(legacy_retry_attempts_or_radio_fem_rxgain)); // 291 + if (legacy_retry_attempts_read == sizeof(legacy_retry_attempts_or_radio_fem_rxgain)) { + _prefs->direct_retry_attempts = legacy_retry_attempts_or_radio_fem_rxgain; + } + file.read((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 292 + file.read((uint8_t *)&_prefs->direct_retry_timing_magic[0], sizeof(_prefs->direct_retry_timing_magic)); // 294 + size_t radio_fem_rxgain_read = file.read((uint8_t *)&_prefs->radio_fem_rxgain, + sizeof(_prefs->radio_fem_rxgain)); // 296 + file.read((uint8_t *)&_prefs->direct_retry_preset, sizeof(_prefs->direct_retry_preset)); // 297 + size_t retry_step_read = file.read((uint8_t *)&_prefs->direct_retry_step_ms, + sizeof(_prefs->direct_retry_step_ms)); // 298 + // PowerSaving-only prefs stored radio_fem_rxgain at 291, before direct retry timing existed. + if (radio_fem_rxgain_read != sizeof(_prefs->radio_fem_rxgain) + && legacy_retry_attempts_read == sizeof(legacy_retry_attempts_or_radio_fem_rxgain) + && (_prefs->direct_retry_timing_magic[0] != DIRECT_RETRY_TIMING_MAGIC_0 + || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1)) { + _prefs->radio_fem_rxgain = constrain(legacy_retry_attempts_or_radio_fem_rxgain, 0, 1); + } + // next: 298 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -103,6 +249,23 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1); _prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 10.0f); _prefs->path_hash_mode = constrain(_prefs->path_hash_mode, 0, 2); // NOTE: mode 3 reserved for future + // Old firmware left offset 108..111 undefined, so require the marker before using the new retry prefs. + if (_prefs->direct_retry_prefs_magic[0] != DIRECT_RETRY_PREFS_MAGIC_0 + || _prefs->direct_retry_prefs_magic[1] != DIRECT_RETRY_PREFS_MAGIC_1) { + _prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT; + _prefs->direct_retry_snr_margin_db = DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT_X4; + } else { + _prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1); + _prefs->direct_retry_snr_margin_db = constrain(_prefs->direct_retry_snr_margin_db, 0, DIRECT_RETRY_SNR_MARGIN_X4_MAX); + } + if (_prefs->direct_retry_timing_magic[0] != DIRECT_RETRY_TIMING_MAGIC_0 + || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1) { + _prefs->direct_retry_attempts = DIRECT_RETRY_COUNT_DEFAULT; + _prefs->direct_retry_base_ms = DIRECT_RETRY_BASE_MS_DEFAULT; + } else { + _prefs->direct_retry_attempts = constrain(_prefs->direct_retry_attempts, DIRECT_RETRY_COUNT_MIN, DIRECT_RETRY_COUNT_MAX); + _prefs->direct_retry_base_ms = constrain(_prefs->direct_retry_base_ms, DIRECT_RETRY_BASE_MS_MIN, DIRECT_RETRY_BASE_MS_MAX); + } // sanitise bad bridge pref values _prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1); @@ -118,6 +281,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // sanitise settings _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean + _prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean + _prefs->direct_retry_preset = directRetryPresetOrDefault(_prefs->direct_retry_preset); + if (retry_step_read != sizeof(_prefs->direct_retry_step_ms)) { + _prefs->direct_retry_step_ms = directRetryPresetStepDefault(_prefs->direct_retry_preset); + } else { + _prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); + } file.close(); } @@ -151,7 +321,11 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84 file.write((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88 file.write((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104 - file.write(pad, 4); // 108 : 4 byte unused + file.write((uint8_t *)&_prefs->direct_retry_recent_enabled, sizeof(_prefs->direct_retry_recent_enabled)); // 108 + file.write((uint8_t *)&_prefs->direct_retry_snr_margin_db, sizeof(_prefs->direct_retry_snr_margin_db)); // 109 + // Persist a marker so later loads can distinguish real values from legacy garbage in this reserved slot. + uint8_t retry_magic[2] = { DIRECT_RETRY_PREFS_MAGIC_0, DIRECT_RETRY_PREFS_MAGIC_1 }; + file.write(retry_magic, sizeof(retry_magic)); // 110 file.write((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112 file.write((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113 file.write((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 @@ -179,7 +353,14 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + file.write((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 291 + file.write((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 292 + uint8_t retry_timing_magic[2] = { DIRECT_RETRY_TIMING_MAGIC_0, DIRECT_RETRY_TIMING_MAGIC_1 }; + file.write(retry_timing_magic, sizeof(retry_timing_magic)); // 294 + file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 296 + file.write((uint8_t *)&_prefs->direct_retry_preset, sizeof(_prefs->direct_retry_preset)); // 297 + file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 298 + // next: 300 file.close(); } @@ -241,7 +422,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else if (memcmp(command, "clock", 5) == 0) { uint32_t now = getRTCClock()->getCurrentTime(); DateTime dt = DateTime(now); - sprintf(reply, "%02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year()); + sprintf(reply, "%02d:%02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(), dt.year()); } else if (memcmp(command, "time ", 5) == 0) { // set time (to epoch seconds) uint32_t secs = _atoi(&command[5]); uint32_t curr = getRTCClock()->getCurrentTime(); @@ -290,9 +471,487 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re _callbacks->clearStats(); strcpy(reply, "(OK - stats reset)"); } else if (memcmp(command, "get ", 4) == 0) { - handleGetCmd(sender_timestamp, command, reply); + const char* config = &command[4]; + if (memcmp(config, "af", 2) == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); + } else if (memcmp(config, "int.thresh", 10) == 0) { + sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); + } else if (memcmp(config, "agc.reset.interval", 18) == 0) { + sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4); + } else if (memcmp(config, "multi.acks", 10) == 0) { + sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks); + } else if (memcmp(config, "allow.read.only", 15) == 0) { + sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off"); + } else if (memcmp(config, "flood.advert.interval", 21) == 0) { + sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval)); + } else if (memcmp(config, "advert.interval", 15) == 0) { + sprintf(reply, "> %d", ((uint32_t) _prefs->advert_interval) * 2); + } else if (memcmp(config, "guest.password", 14) == 0) { + sprintf(reply, "> %s", _prefs->guest_password); + } else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) { // from serial command line only + uint8_t prv_key[PRV_KEY_SIZE]; + int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE); + mesh::Utils::toHex(tmp, prv_key, len); + sprintf(reply, "> %s", tmp); + } else if (memcmp(config, "name", 4) == 0) { + sprintf(reply, "> %s", _prefs->node_name); + } else if (memcmp(config, "repeat", 6) == 0) { + sprintf(reply, "> %s", _prefs->disable_fwd ? "off" : "on"); + } else if (memcmp(config, "lat", 3) == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat)); + } else if (memcmp(config, "lon", 3) == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon)); +#if defined(USE_SX1262) || defined(USE_SX1268) + } else if (memcmp(config, "radio.rxgain", 12) == 0) { + sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off"); +#endif + } else if (memcmp(config, "radio", 5) == 0) { + char freq[16], bw[16]; + strcpy(freq, StrHelper::ftoa(_prefs->freq)); + strcpy(bw, StrHelper::ftoa3(_prefs->bw)); + sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr); + } else if (memcmp(config, "rxdelay", 7) == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base)); + } else if (memcmp(config, "txdelay", 7) == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor)); + } else if (memcmp(config, "flood.max", 9) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); + } else if (memcmp(config, "direct.txdelay", 14) == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); + } else if (memcmp(config, "direct.retry.heard", 18) == 0) { + sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); + } else if (memcmp(config, "direct.retry.margin", 19) == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(directRetryMarginX4ToDb(directRetryEffectiveMarginX4(_prefs)))); + } else if (memcmp(config, "direct.retry.preset", 19) == 0) { + sprintf(reply, "> %d,%s", + (uint32_t)directRetryPresetOrDefault(_prefs->direct_retry_preset), + directRetryPresetName(_prefs->direct_retry_preset)); + } else if (memcmp(config, "direct.retry.count", 18) == 0) { + sprintf(reply, "> %d", (uint32_t)directRetryEffectiveCount(_prefs)); + } else if (memcmp(config, "direct.retry.base", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)directRetryEffectiveBaseMs(_prefs)); + } else if (memcmp(config, "direct.retry.step", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)directRetryEffectiveStepMs(_prefs)); + } else if (memcmp(config, "owner.info", 10) == 0) { + *reply++ = '>'; + *reply++ = ' '; + const char* sp = _prefs->owner_info; + while (*sp) { + *reply++ = (*sp == '\n') ? '|' : *sp; // translate newline back to orig '|' + sp++; + } + *reply = 0; // set null terminator + } else if (memcmp(config, "path.hash.mode", 14) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode); + } else if (memcmp(config, "loop.detect", 11) == 0) { + if (_prefs->loop_detect == LOOP_DETECT_OFF) { + strcpy(reply, "> off"); + } else if (_prefs->loop_detect == LOOP_DETECT_MINIMAL) { + strcpy(reply, "> minimal"); + } else if (_prefs->loop_detect == LOOP_DETECT_MODERATE) { + strcpy(reply, "> moderate"); + } else { + strcpy(reply, "> strict"); + } + } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) { + sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm); + } else if (memcmp(config, "freq", 4) == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq)); + } else if (memcmp(config, "public.key", 10) == 0) { + strcpy(reply, "> "); + mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE); + } else if (memcmp(config, "role", 4) == 0) { + sprintf(reply, "> %s", _callbacks->getRole()); + } else if (memcmp(config, "bridge.type", 11) == 0) { + sprintf(reply, "> %s", +#ifdef WITH_RS232_BRIDGE + "rs232" +#elif WITH_ESPNOW_BRIDGE + "espnow" +#else + "none" +#endif + ); +#ifdef WITH_BRIDGE + } else if (memcmp(config, "bridge.enabled", 14) == 0) { + sprintf(reply, "> %s", _prefs->bridge_enabled ? "on" : "off"); + } else if (memcmp(config, "bridge.delay", 12) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->bridge_delay); + } else if (memcmp(config, "bridge.source", 13) == 0) { + sprintf(reply, "> %s", _prefs->bridge_pkt_src ? "logRx" : "logTx"); +#endif +#ifdef WITH_RS232_BRIDGE + } else if (memcmp(config, "bridge.baud", 11) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud); +#endif +#ifdef WITH_ESPNOW_BRIDGE + } else if (memcmp(config, "bridge.channel", 14) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel); + } else if (memcmp(config, "bridge.secret", 13) == 0) { + sprintf(reply, "> %s", _prefs->bridge_secret); +#endif + } else if (memcmp(config, "bootloader.ver", 14) == 0) { + #ifdef NRF52_PLATFORM + char ver[32]; + if (_board->getBootloaderVersion(ver, sizeof(ver))) { + sprintf(reply, "> %s", ver); + } else { + strcpy(reply, "> unknown"); + } + #else + strcpy(reply, "ERROR: unsupported"); + #endif + } else if (memcmp(config, "adc.multiplier", 14) == 0) { + float adc_mult = _board->getAdcMultiplier(); + if (adc_mult == 0.0f) { + strcpy(reply, "Error: unsupported by this board"); + } else { + sprintf(reply, "> %.3f", adc_mult); + } + // Power management commands + } else if (memcmp(config, "pwrmgt.support", 14) == 0) { +#ifdef NRF52_POWER_MANAGEMENT + strcpy(reply, "> supported"); +#else + strcpy(reply, "> unsupported"); +#endif + } else if (memcmp(config, "pwrmgt.source", 13) == 0) { +#ifdef NRF52_POWER_MANAGEMENT + strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery"); +#else + strcpy(reply, "ERROR: Power management not supported"); +#endif + } else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) { +#ifdef NRF52_POWER_MANAGEMENT + sprintf(reply, "> Reset: %s; Shutdown: %s", + _board->getResetReasonString(_board->getResetReason()), + _board->getShutdownReasonString(_board->getShutdownReason())); +#else + strcpy(reply, "ERROR: Power management not supported"); +#endif + } else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) { +#ifdef NRF52_POWER_MANAGEMENT + sprintf(reply, "> %u mV", _board->getBootVoltage()); +#else + strcpy(reply, "ERROR: Power management not supported"); +#endif + } else { + sprintf(reply, "??: %s", config); + } + /* + * SET commands + */ } else if (memcmp(command, "set ", 4) == 0) { - handleSetCmd(sender_timestamp, command, reply); + const char* config = &command[4]; + if (memcmp(config, "af ", 3) == 0) { + _prefs->airtime_factor = atof(&config[3]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "int.thresh ", 11) == 0) { + _prefs->interference_threshold = atoi(&config[11]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "agc.reset.interval ", 19) == 0) { + _prefs->agc_reset_interval = atoi(&config[19]) / 4; + savePrefs(); + sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4); + } else if (memcmp(config, "multi.acks ", 11) == 0) { + _prefs->multi_acks = atoi(&config[11]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "allow.read.only ", 16) == 0) { + _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "flood.advert.interval ", 22) == 0) { + int hours = _atoi(&config[22]); + if ((hours > 0 && hours < 3) || (hours > 168)) { + strcpy(reply, "Error: interval range is 3-168 hours"); + } else { + _prefs->flood_advert_interval = (uint8_t)(hours); + _callbacks->updateFloodAdvertTimer(); + savePrefs(); + strcpy(reply, "OK"); + } + } else if (memcmp(config, "advert.interval ", 16) == 0) { + int mins = _atoi(&config[16]); + if ((mins > 0 && mins < MIN_LOCAL_ADVERT_INTERVAL) || (mins > 240)) { + sprintf(reply, "Error: interval range is %d-240 minutes", MIN_LOCAL_ADVERT_INTERVAL); + } else { + _prefs->advert_interval = (uint8_t)(mins / 2); + _callbacks->updateAdvertTimer(); + savePrefs(); + strcpy(reply, "OK"); + } + } else if (memcmp(config, "guest.password ", 15) == 0) { + StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password)); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "prv.key ", 8) == 0) { + uint8_t prv_key[PRV_KEY_SIZE]; + bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]); + // only allow rekey if key is valid + if (success && mesh::LocalIdentity::validatePrivateKey(prv_key)) { + mesh::LocalIdentity new_id; + new_id.readFrom(prv_key, PRV_KEY_SIZE); + _callbacks->saveIdentity(new_id); + strcpy(reply, "OK, reboot to apply! New pubkey: "); + mesh::Utils::toHex(&reply[33], new_id.pub_key, PUB_KEY_SIZE); + } else { + strcpy(reply, "Error, bad key"); + } + } else if (memcmp(config, "name ", 5) == 0) { + if (isValidName(&config[5])) { + StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name)); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, bad chars"); + } + } else if (memcmp(config, "repeat ", 7) == 0) { + _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0; + savePrefs(); + strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); +#if defined(USE_SX1262) || defined(USE_SX1268) + } else if (memcmp(config, "radio.rxgain ", 13) == 0) { + _prefs->rx_boosted_gain = memcmp(&config[13], "on", 2) == 0; + strcpy(reply, "OK"); + savePrefs(); + _callbacks->setRxBoostedGain(_prefs->rx_boosted_gain); +#endif + } else if (memcmp(config, "radio ", 6) == 0) { + strcpy(tmp, &config[6]); + const char *parts[4]; + int num = mesh::Utils::parseTextParts(tmp, parts, 4); + float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f; + float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f; + uint8_t sf = num > 2 ? atoi(parts[2]) : 0; + uint8_t cr = num > 3 ? atoi(parts[3]) : 0; + if (freq >= 300.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) { + _prefs->sf = sf; + _prefs->cr = cr; + _prefs->freq = freq; + _prefs->bw = bw; + _callbacks->savePrefs(); + strcpy(reply, "OK - reboot to apply"); + } else { + strcpy(reply, "Error, invalid radio params"); + } + } else if (memcmp(config, "lat ", 4) == 0) { + _prefs->node_lat = atof(&config[4]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "lon ", 4) == 0) { + _prefs->node_lon = atof(&config[4]); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "rxdelay ", 8) == 0) { + float db = atof(&config[8]); + if (db >= 0) { + _prefs->rx_delay_base = db; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, cannot be negative"); + } + } else if (memcmp(config, "txdelay ", 8) == 0) { + float f = atof(&config[8]); + if (f >= 0) { + _prefs->tx_delay_factor = f; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, cannot be negative"); + } + } else if (memcmp(config, "flood.max ", 10) == 0) { + uint8_t m = atoi(&config[10]); + if (m <= 64) { + _prefs->flood_max = m; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, max 64"); + } + } else if (memcmp(config, "direct.txdelay ", 15) == 0) { + float f = atof(&config[15]); + if (f >= 0) { + _prefs->direct_tx_delay_factor = f; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, cannot be negative"); + } + } else if (memcmp(config, "direct.retry.heard ", 19) == 0) { + if (memcmp(&config[19], "on", 2) == 0) { + _prefs->direct_retry_recent_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(&config[19], "off", 3) == 0) { + _prefs->direct_retry_recent_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be on or off"); + } + } else if (memcmp(config, "direct.retry.margin ", 20) == 0) { + float db = atof(&config[20]); + if (db >= 0 && db <= DIRECT_RETRY_SNR_MARGIN_DB_MAX) { + _prefs->direct_retry_snr_margin_db = directRetryMarginDbToX4(db); + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min 0 and max %d", DIRECT_RETRY_SNR_MARGIN_DB_MAX); + } + } else if (memcmp(config, "direct.retry.preset ", 20) == 0) { + uint8_t preset; + if (parseDirectRetryPreset(&config[20], preset)) { + applyDirectRetryPreset(_prefs, preset); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be infra, rooftop, mobile, 0, 1, or 2"); + } + } else if (memcmp(config, "direct.retry.count ", 19) == 0) { + int count = atoi(&config[19]); + if (count >= DIRECT_RETRY_COUNT_MIN && count <= DIRECT_RETRY_COUNT_MAX) { + _prefs->direct_retry_attempts = (uint8_t)count; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_COUNT_MIN, DIRECT_RETRY_COUNT_MAX); + } + } else if (memcmp(config, "direct.retry.base ", 18) == 0) { + int delay_ms = atoi(&config[18]); + if (delay_ms >= DIRECT_RETRY_BASE_MS_MIN && delay_ms <= DIRECT_RETRY_BASE_MS_MAX) { + _prefs->direct_retry_base_ms = (uint16_t)delay_ms; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_BASE_MS_MIN, DIRECT_RETRY_BASE_MS_MAX); + } + } else if (memcmp(config, "direct.retry.step ", 18) == 0) { + int delay_ms = atoi(&config[18]); + if (delay_ms >= DIRECT_RETRY_STEP_MS_MIN && delay_ms <= DIRECT_RETRY_STEP_MS_MAX) { + _prefs->direct_retry_step_ms = (uint16_t)delay_ms; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); + } + } else if (memcmp(config, "owner.info ", 11) == 0) { + config += 11; + char *dp = _prefs->owner_info; + while (*config && dp - _prefs->owner_info < sizeof(_prefs->owner_info)-1) { + *dp++ = (*config == '|') ? '\n' : *config; // translate '|' to newline chars + config++; + } + *dp = 0; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "path.hash.mode ", 15) == 0) { + config += 15; + uint8_t mode = atoi(config); + if (mode < 3) { + _prefs->path_hash_mode = mode; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0,1, or 2"); + } + } else if (memcmp(config, "loop.detect ", 12) == 0) { + config += 12; + uint8_t mode; + if (memcmp(config, "off", 3) == 0) { + mode = LOOP_DETECT_OFF; + } else if (memcmp(config, "minimal", 7) == 0) { + mode = LOOP_DETECT_MINIMAL; + } else if (memcmp(config, "moderate", 8) == 0) { + mode = LOOP_DETECT_MODERATE; + } else if (memcmp(config, "strict", 6) == 0) { + mode = LOOP_DETECT_STRICT; + } else { + mode = 0xFF; + strcpy(reply, "Error, must be: off, minimal, moderate, or strict"); + } + if (mode != 0xFF) { + _prefs->loop_detect = mode; + savePrefs(); + strcpy(reply, "OK"); + } + } else if (memcmp(config, "tx ", 3) == 0) { + _prefs->tx_power_dbm = atoi(&config[3]); + savePrefs(); + _callbacks->setTxPower(_prefs->tx_power_dbm); + strcpy(reply, "OK"); + } else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) { + _prefs->freq = atof(&config[5]); + savePrefs(); + strcpy(reply, "OK - reboot to apply"); +#ifdef WITH_BRIDGE + } else if (memcmp(config, "bridge.enabled ", 15) == 0) { + _prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 0; + _callbacks->setBridgeState(_prefs->bridge_enabled); + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "bridge.delay ", 13) == 0) { + int delay = _atoi(&config[13]); + if (delay >= 0 && delay <= 10000) { + _prefs->bridge_delay = (uint16_t)delay; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error: delay must be between 0-10000 ms"); + } + } else if (memcmp(config, "bridge.source ", 14) == 0) { + _prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0; + savePrefs(); + strcpy(reply, "OK"); +#endif +#ifdef WITH_RS232_BRIDGE + } else if (memcmp(config, "bridge.baud ", 12) == 0) { + uint32_t baud = atoi(&config[12]); + if (baud >= 9600 && baud <= BRIDGE_MAX_BAUD) { + _prefs->bridge_baud = (uint32_t)baud; + _callbacks->restartBridge(); + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD); + } +#endif +#ifdef WITH_ESPNOW_BRIDGE + } else if (memcmp(config, "bridge.channel ", 15) == 0) { + int ch = atoi(&config[15]); + if (ch > 0 && ch < 15) { + _prefs->bridge_channel = (uint8_t)ch; + _callbacks->restartBridge(); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error: channel must be between 1-14"); + } + } else if (memcmp(config, "bridge.secret ", 14) == 0) { + StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret)); + _callbacks->restartBridge(); + savePrefs(); + strcpy(reply, "OK"); +#endif + } else if (memcmp(config, "adc.multiplier ", 15) == 0) { + _prefs->adc_multiplier = atof(&config[15]); + if (_board->setAdcMultiplier(_prefs->adc_multiplier)) { + savePrefs(); + if (_prefs->adc_multiplier == 0.0f) { + strcpy(reply, "OK - using default board multiplier"); + } else { + sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier); + } + } else { + _prefs->adc_multiplier = 0.0f; + strcpy(reply, "Error: unsupported by this board"); + }; + } else { + sprintf(reply, "unknown config: %s", config); + } } else if (sender_timestamp == 0 && strcmp(command, "erase") == 0) { bool s = _callbacks->formatFileSystem(); sprintf(reply, "File system erase: %s", s ? "OK" : "Err"); @@ -426,19 +1085,59 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } #endif } else if (memcmp(command, "powersaving on", 14) == 0) { +#if defined(NRF52_PLATFORM) + _prefs->powersaving_enabled = 1; + savePrefs(); + strcpy(reply, "On - Immediate effect"); +#elif defined(ESP32) && !defined(WITH_BRIDGE) _prefs->powersaving_enabled = 1; savePrefs(); - strcpy(reply, "ok"); // TODO: to return Not supported if required + strcpy(reply, "On - After 2 minutes"); +#elif defined(WITH_BRIDGE) + strcpy(reply, "Bridge not supported"); +#else + strcpy(reply, "Board not supported"); +#endif } else if (memcmp(command, "powersaving off", 15) == 0) { _prefs->powersaving_enabled = 0; savePrefs(); - strcpy(reply, "ok"); + strcpy(reply, "Off"); } else if (memcmp(command, "powersaving", 11) == 0) { if (_prefs->powersaving_enabled) { - strcpy(reply, "on"); + strcpy(reply, "On"); } else { - strcpy(reply, "off"); + strcpy(reply, "Off"); } + } else if (memcmp(command, "sensor", 6) == 0) { + // I2C +#if defined(ENV_PIN_SDA) && defined(ENV_PIN_SCL) + sprintf(reply, "I2C Wire1: SDA=%s,SCL=%s\r\n", STR(ENV_PIN_SDA), STR(ENV_PIN_SCL)); +#elif defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) + sprintf(reply, "I2C Wire: SDA=%s, SCL=%s\r\n", STR(PIN_BOARD_SDA), STR(PIN_BOARD_SCL)); +#elif defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL) + sprintf(reply, "I2C Wire: SDA=%s, SCL=%s\r\n", STR(PIN_WIRE_SDA), STR(PIN_WIRE_SCL)); +#else + sprintf(reply, "I2C GPIOs not defined\r\n"); +#endif + + // GPS +#if defined(PIN_GPS_RX) && defined(PIN_GPS_TX) + sprintf(reply + strlen(reply), "GPS Serial: RX=%s, TX=%s", STR(PIN_GPS_RX), STR(PIN_GPS_TX)); +#ifdef ENV_INCLUDE_GPS> 0 + sprintf(reply + strlen(reply), ". Configured"); +#else + sprintf(reply + strlen(reply), ". Not configured"); +#endif +#else + sprintf(reply + strlen(reply), "GPS Serial not defined"); +#endif + } else if (memcmp(command, "powerlog", 8) == 0) { + sprintf(reply, "Last reset reason: %s", _board->getResetReasonString(_board->getResetReason())); +#if defined(NRF52_PLATFORM) + sprintf(reply + strlen(reply), "\r\nLast shutdown reason: %s", + _board->getShutdownReasonString(_board->getShutdownReason())); + sprintf(reply + strlen(reply), "\r\nLast boot voltage: %u mV", _board->getBootVoltage()); +#endif } else if (memcmp(command, "log start", 9) == 0) { _callbacks->setLoggingOn(true); strcpy(reply, " logging on"); @@ -552,6 +1251,28 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep savePrefs(); _callbacks->setRxBoostedGain(_prefs->rx_boosted_gain); #endif + } else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) { + if (!_board->canControlLoRaFemLna()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&config[17], "on", 2) == 0) { + if (_board->setLoRaFemLnaEnabled(true)) { + _prefs->radio_fem_rxgain = 1; + savePrefs(); + strcpy(reply, "OK - LoRa FEM RX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else if (memcmp(&config[17], "off", 3) == 0) { + if (_board->setLoRaFemLnaEnabled(false)) { + _prefs->radio_fem_rxgain = 0; + savePrefs(); + strcpy(reply, "OK - LoRa FEM RX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } } else if (memcmp(config, "radio ", 6) == 0) { strcpy(tmp, &config[6]); const char *parts[4]; @@ -614,6 +1335,63 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, cannot be negative"); } + } else if (memcmp(config, "direct.retry.heard ", 19) == 0) { + if (memcmp(&config[19], "on", 2) == 0) { + _prefs->direct_retry_recent_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(&config[19], "off", 3) == 0) { + _prefs->direct_retry_recent_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be on or off"); + } + } else if (memcmp(config, "direct.retry.margin ", 20) == 0) { + float db = atof(&config[20]); + if (db >= 0 && db <= DIRECT_RETRY_SNR_MARGIN_DB_MAX) { + _prefs->direct_retry_snr_margin_db = directRetryMarginDbToX4(db); + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min 0 and max %d", DIRECT_RETRY_SNR_MARGIN_DB_MAX); + } + } else if (memcmp(config, "direct.retry.preset ", 20) == 0) { + uint8_t preset; + if (parseDirectRetryPreset(&config[20], preset)) { + applyDirectRetryPreset(_prefs, preset); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be infra, rooftop, mobile, 0, 1, or 2"); + } + } else if (memcmp(config, "direct.retry.count ", 19) == 0) { + int count = atoi(&config[19]); + if (count >= DIRECT_RETRY_COUNT_MIN && count <= DIRECT_RETRY_COUNT_MAX) { + _prefs->direct_retry_attempts = (uint8_t)count; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_COUNT_MIN, DIRECT_RETRY_COUNT_MAX); + } + } else if (memcmp(config, "direct.retry.base ", 18) == 0) { + int delay_ms = atoi(&config[18]); + if (delay_ms >= DIRECT_RETRY_BASE_MS_MIN && delay_ms <= DIRECT_RETRY_BASE_MS_MAX) { + _prefs->direct_retry_base_ms = (uint16_t)delay_ms; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_BASE_MS_MIN, DIRECT_RETRY_BASE_MS_MAX); + } + } else if (memcmp(config, "direct.retry.step ", 18) == 0) { + int delay_ms = atoi(&config[18]); + if (delay_ms >= DIRECT_RETRY_STEP_MS_MIN && delay_ms <= DIRECT_RETRY_STEP_MS_MAX) { + _prefs->direct_retry_step_ms = (uint16_t)delay_ms; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; @@ -723,10 +1501,10 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } } else { _prefs->adc_multiplier = 0.0f; - strcpy(reply, "Error: unsupported by this board"); + strcpy(reply, "Error: unsupported"); }; } else { - sprintf(reply, "unknown config: %s", config); + strcpy(reply, "unknown config: "); } } @@ -770,6 +1548,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else if (memcmp(config, "radio.rxgain", 12) == 0) { sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off"); #endif + } else if (memcmp(config, "radio.fem.rxgain", 16) == 0) { + if (!_board->canControlLoRaFemLna()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off"); + } } else if (memcmp(config, "radio", 5) == 0) { char freq[16], bw[16]; strcpy(freq, StrHelper::ftoa(_prefs->freq)); @@ -783,6 +1567,20 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); } else if (memcmp(config, "direct.txdelay", 14) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); + } else if (memcmp(config, "direct.retry.heard", 18) == 0) { + sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); + } else if (memcmp(config, "direct.retry.margin", 19) == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(directRetryMarginX4ToDb(directRetryEffectiveMarginX4(_prefs)))); + } else if (memcmp(config, "direct.retry.preset", 19) == 0) { + sprintf(reply, "> %d,%s", + (uint32_t)directRetryPresetOrDefault(_prefs->direct_retry_preset), + directRetryPresetName(_prefs->direct_retry_preset)); + } else if (memcmp(config, "direct.retry.count", 18) == 0) { + sprintf(reply, "> %d", (uint32_t)directRetryEffectiveCount(_prefs)); + } else if (memcmp(config, "direct.retry.base", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)directRetryEffectiveBaseMs(_prefs)); + } else if (memcmp(config, "direct.retry.step", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)directRetryEffectiveStepMs(_prefs)); } else if (memcmp(config, "owner.info", 10) == 0) { *reply++ = '>'; *reply++ = ' '; @@ -850,12 +1648,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "> unknown"); } #else - strcpy(reply, "ERROR: unsupported"); + strcpy(reply, "Error: unsupported"); #endif } else if (memcmp(config, "adc.multiplier", 14) == 0) { float adc_mult = _board->getAdcMultiplier(); if (adc_mult == 0.0f) { - strcpy(reply, "Error: unsupported by this board"); + strcpy(reply, "Error: unsupported"); } else { sprintf(reply, "> %.3f", adc_mult); } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index ffdc7c6536..ea30777a2d 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -19,6 +19,25 @@ #define LOOP_DETECT_MODERATE 2 #define LOOP_DETECT_STRICT 3 +#define DIRECT_RETRY_PRESET_INFRA 0 +#define DIRECT_RETRY_PRESET_ROOFTOP 1 +#define DIRECT_RETRY_PRESET_MOBILE 2 + +#define DIRECT_RETRY_INFRA_BASE_MS 275 +#define DIRECT_RETRY_INFRA_COUNT 4 +#define DIRECT_RETRY_INFRA_STEP_MS 150 +#define DIRECT_RETRY_INFRA_MARGIN_X4 60 + +#define DIRECT_RETRY_ROOFTOP_BASE_MS 175 +#define DIRECT_RETRY_ROOFTOP_COUNT 15 +#define DIRECT_RETRY_ROOFTOP_STEP_MS 100 +#define DIRECT_RETRY_ROOFTOP_MARGIN_X4 20 + +#define DIRECT_RETRY_MOBILE_BASE_MS 175 +#define DIRECT_RETRY_MOBILE_COUNT 15 +#define DIRECT_RETRY_MOBILE_STEP_MS 50 +#define DIRECT_RETRY_MOBILE_MARGIN_X4 0 + struct NodePrefs { // persisted to file float airtime_factor; char node_name[32]; @@ -33,7 +52,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 +80,14 @@ 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 direct_retry_preset; + uint16_t direct_retry_step_ms; }; class CommonCLICallbacks { diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index c2d78ae08f..fe9865931d 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -12,12 +12,14 @@ #include #include #include -#include "driver/rtc_io.h" +#include "soc/rtc.h" +#include "esp_system.h" class ESP32Board : public mesh::MainBoard { protected: uint8_t startup_reason; bool inhibit_sleep = false; + static inline portMUX_TYPE sleepMux = portMUX_INITIALIZER_UNLOCKED; public: void begin() { @@ -60,25 +62,48 @@ class ESP32Board : public mesh::MainBoard { return raw / 4; } - void enterLightSleep(uint32_t secs) { -#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(P_LORA_DIO_1) // Supported ESP32 variants - if (rtc_gpio_is_valid_gpio((gpio_num_t)P_LORA_DIO_1)) { // Only enter sleep mode if P_LORA_DIO_1 is RTC pin - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // To wake up when receiving a LoRa packet + uint32_t getIRQGpio() { + return P_LORA_DIO_1; // default for SX1262 + } + + void sleep(uint32_t secs) override { + // Skip if not allow to sleep + if (inhibit_sleep) { + delay(1); // Give MCU to OTA to run + return; + } - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); // To wake up every hour to do periodically jobs - } + // Set GPIO wakeup + gpio_num_t wakeupPin = (gpio_num_t)getIRQGpio(); - esp_light_sleep_start(); // CPU enters light sleep + // Configure timer wakeup + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000ULL); // Wake up periodically to do scheduled jobs } -#endif - } - void sleep(uint32_t secs) override { - if (!inhibit_sleep) { - enterLightSleep(secs); // To wake up after "secs" seconds or when receiving a LoRa packet + // 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 @@ class ESP32Board : public mesh::MainBoard { #endif uint16_t getBattMilliVolts() override { - #ifdef PIN_VBAT_READ + #ifdef PIN_VBAT_READ analogReadResolution(12); uint32_t raw = 0; @@ -130,31 +155,88 @@ class ESP32Board : public mesh::MainBoard { void setInhibitSleep(bool inhibit) { inhibit_sleep = inhibit; } + + uint32_t getResetReason() const override { + return esp_reset_reason(); + } + + // https://docs.espressif.com/projects/esp-idf/en/v4.4.7/esp32/api-reference/system/system.html + const char *getResetReasonString(uint32_t reason) { + switch (reason) { + case ESP_RST_UNKNOWN: + return "Unknown or first boot"; + case ESP_RST_POWERON: + return "Power-on reset"; + case ESP_RST_EXT: + return "External reset"; + case ESP_RST_SW: + return "Software reset"; + case ESP_RST_PANIC: + return "Panic / exception reset"; + case ESP_RST_INT_WDT: + return "Interrupt watchdog reset"; + case ESP_RST_TASK_WDT: + return "Task watchdog reset"; + case ESP_RST_WDT: + return "Other watchdog reset"; + case ESP_RST_DEEPSLEEP: + return "Wake from deep sleep"; + case ESP_RST_BROWNOUT: + return "Brownout (low voltage)"; + case ESP_RST_SDIO: + return "SDIO reset"; + default: + static char buf[40]; + snprintf(buf, sizeof(buf), "Unknown reset reason (%d)", reason); + return buf; + } + } }; +static RTC_NOINIT_ATTR uint32_t _rtc_backup_time; +static RTC_NOINIT_ATTR uint32_t _rtc_backup_magic; +#define RTC_BACKUP_MAGIC 0xAA55CC33 +#define RTC_TIME_MIN 1772323200 // 1 Mar 2026 + class ESP32RTCClock : public mesh::RTCClock { public: ESP32RTCClock() { } void begin() { esp_reset_reason_t reason = esp_reset_reason(); - if (reason == ESP_RST_POWERON) { - // start with some date/time in the recent past - struct timeval tv; - tv.tv_sec = 1715770351; // 15 May 2024, 8:50pm - tv.tv_usec = 0; - settimeofday(&tv, NULL); + if (reason == ESP_RST_DEEPSLEEP) { + return; // ESP-IDF preserves system time across deep sleep } + // All other resets (power-on, crash, WDT, brownout) lose system time. + // Restore from RTC backup if valid, otherwise use hardcoded seed. + struct timeval tv; + if (_rtc_backup_magic == RTC_BACKUP_MAGIC && _rtc_backup_time > RTC_TIME_MIN) { + tv.tv_sec = _rtc_backup_time; + } else { + tv.tv_sec = 1772323200; // 1 Mar 2026 + } + tv.tv_usec = 0; + settimeofday(&tv, NULL); } uint32_t getCurrentTime() override { time_t _now; time(&_now); return _now; } - void setCurrentTime(uint32_t time) override { + void setCurrentTime(uint32_t time) override { struct timeval tv; tv.tv_sec = time; tv.tv_usec = 0; settimeofday(&tv, NULL); + _rtc_backup_time = time; + _rtc_backup_magic = RTC_BACKUP_MAGIC; + } + void tick() override { + time_t now; + time(&now); + if (now > RTC_TIME_MIN && (uint32_t)now != _rtc_backup_time) { + _rtc_backup_time = (uint32_t)now; + _rtc_backup_magic = RTC_BACKUP_MAGIC; + } } }; diff --git a/src/helpers/SensorManager.h b/src/helpers/SensorManager.h index 89a174c228..d4aa63b70f 100644 --- a/src/helpers/SensorManager.h +++ b/src/helpers/SensorManager.h @@ -2,6 +2,7 @@ #include #include "sensors/LocationProvider.h" +#include #define TELEM_PERM_BASE 0x01 // 'base' permission includes battery #define TELEM_PERM_LOCATION 0x02 @@ -15,6 +16,7 @@ class SensorManager { double node_altitude; // altitude in meters SensorManager() { node_lat = 0; node_lon = 0; node_altitude = 0; } + virtual bool i2c_probe(TwoWire& wire, uint8_t addr) { return false; } virtual bool begin() { return false; } virtual bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { return false; } virtual void loop() { } diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 2f8af52af1..ae28acc8fd 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -8,13 +8,137 @@ #define MAX_PACKET_HASHES 128 #define MAX_PACKET_ACKS 64 +#ifndef MAX_RECENT_REPEATERS + // Platform defaults. Can be overridden with -D MAX_RECENT_REPEATERS=. + #if defined(ESP32) || defined(ESP32_PLATFORM) + #define MAX_RECENT_REPEATERS 2048 + #elif defined(NRF52_PLATFORM) + #define MAX_RECENT_REPEATERS 512 + #else + #define MAX_RECENT_REPEATERS 64 + #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; + }; + +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 +147,11 @@ class SimpleMeshTables : public mesh::MeshTables { memset(_acks, 0, sizeof(_acks)); _next_ack_idx = 0; _direct_dups = _flood_dups = 0; + memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); + _next_recent_repeater_idx = 0; + _recent_repeater_min_snr_x4 = -128; + _recent_repeater_allow_fn = NULL; + _recent_repeater_allow_ctx = NULL; } #ifdef ESP32 @@ -31,12 +160,19 @@ class SimpleMeshTables : public mesh::MeshTables { f.read((uint8_t *) &_next_idx, sizeof(_next_idx)); f.read((uint8_t *) &_acks[0], sizeof(_acks)); f.read((uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx)); + // 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)); f.write((const uint8_t *) &_next_idx, sizeof(_next_idx)); f.write((const uint8_t *) &_acks[0], sizeof(_acks)); f.write((const uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx)); + f.write((const uint8_t *) &_recent_repeaters[0], sizeof(_recent_repeaters)); + f.write((const uint8_t *) &_next_recent_repeater_idx, sizeof(_next_recent_repeater_idx)); } #endif @@ -44,42 +180,55 @@ class SimpleMeshTables : public mesh::MeshTables { if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { uint32_t ack; memcpy(&ack, packet->payload, 4); - for (int i = 0; i < MAX_PACKET_ACKS; i++) { - if (ack == _acks[i]) { - if (packet->isRouteDirect()) { - _direct_dups++; // keep some stats - } else { - _flood_dups++; - } - return true; + + if (hasSeenAck(ack)) { + if (packet->isRouteDirect()) { + _direct_dups++; // keep some stats + } else { + _flood_dups++; } + return true; } - - _acks[_next_ack_idx] = ack; - _next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS; // cyclic table + + storeAck(ack); return false; } uint8_t hash[MAX_HASH_SIZE]; packet->calculatePacketHash(hash); - const uint8_t* sp = _hashes; - for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { - if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { - if (packet->isRouteDirect()) { - _direct_dups++; // keep some stats - } else { - _flood_dups++; - } - return true; + if (hasSeenHash(hash)) { + if (packet->isRouteDirect()) { + _direct_dups++; // keep some stats + } else { + _flood_dups++; } + return true; } - memcpy(&_hashes[_next_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE); - _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; // cyclic table + storeHash(hash); + recordRecentRepeater(packet); return false; } + void markSent(const mesh::Packet* packet) override { + // Outbound packets must be marked as already-sent without teaching the recent-heard cache about ourselves. + if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { + uint32_t ack; + memcpy(&ack, packet->payload, 4); + if (!hasSeenAck(ack)) { + storeAck(ack); + } + return; + } + + uint8_t hash[MAX_HASH_SIZE]; + packet->calculatePacketHash(hash); + if (!hasSeenHash(hash)) { + storeHash(hash); + } + } + void clear(const mesh::Packet* packet) override { if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { uint32_t ack; @@ -107,5 +256,185 @@ class SimpleMeshTables : public mesh::MeshTables { uint32_t getNumDirectDups() const { return _direct_dups; } uint32_t getNumFloodDups() const { return _flood_dups; } + void setRecentRepeaterMinSNRX4(int8_t min_snr_x4) { + _recent_repeater_min_snr_x4 = min_snr_x4; + } + void setRecentRepeaterAllowFilter(RecentRepeaterAllowFn fn, void* ctx) { + _recent_repeater_allow_fn = fn; + _recent_repeater_allow_ctx = ctx; + } + bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4, 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); + } + 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; + _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; } }; diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index dcfa0e1e34..50e1501e5e 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -182,6 +182,10 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { #define BLE_WRITE_MIN_INTERVAL 60 +bool SerialBLEInterface::isReadBusy() const { + return (recv_queue_len > 0); +} + bool SerialBLEInterface::isWriteBusy() const { return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write? } diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 965e90fd19..19e024b040 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -76,6 +76,7 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE bool isConnected() const override; + bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; diff --git a/src/helpers/esp32/TBeamBoard.h b/src/helpers/esp32/TBeamBoard.h index 4ff9555103..98bd16bff4 100644 --- a/src/helpers/esp32/TBeamBoard.h +++ b/src/helpers/esp32/TBeamBoard.h @@ -59,13 +59,13 @@ // uint32_t P_LORA_BUSY = 0; //shared, so define at run // uint32_t P_LORA_DIO_2 = 0; //SX1276 only, so define at run - #define P_LORA_DIO_0 26 - #define P_LORA_DIO_1 33 - #define P_LORA_NSS 18 - #define P_LORA_RESET 23 - #define P_LORA_SCLK 5 - #define P_LORA_MISO 19 - #define P_LORA_MOSI 27 + // #define P_LORA_DIO_0 26 + // #define P_LORA_DIO_1 33 + // #define P_LORA_NSS 18 + // #define P_LORA_RESET 23 + // #define P_LORA_SCLK 5 + // #define P_LORA_MISO 19 + // #define P_LORA_MOSI 27 // #define PIN_GPS_RX 34 // #define PIN_GPS_TX 12 diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index 75a4e3b064..a846e744ed 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -401,6 +401,10 @@ bool SerialBLEInterface::isConnected() const { return _isDeviceConnected && Bluefruit.connected() > 0; } +bool SerialBLEInterface::isReadBusy() const { + return (recv_queue_len > 0); +} + bool SerialBLEInterface::isWriteBusy() const { return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3); } diff --git a/src/helpers/nrf52/SerialBLEInterface.h b/src/helpers/nrf52/SerialBLEInterface.h index de1030548f..d3cc505516 100644 --- a/src/helpers/nrf52/SerialBLEInterface.h +++ b/src/helpers/nrf52/SerialBLEInterface.h @@ -66,6 +66,7 @@ class SerialBLEInterface : public BaseSerialInterface { void disable() override; bool isEnabled() const override { return _isEnabled; } bool isConnected() const override; + bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 19472406d8..749ea68971 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -7,8 +7,10 @@ #endif #ifdef ENV_INCLUDE_BME680 -#ifndef TELEM_BME680_ADDRESS -#define TELEM_BME680_ADDRESS 0x76 +#if defined(TELEM_BME680_ADDRESS) +uint8_t TELEM_BME680_ADDRESSES[] = { TELEM_BME680_ADDRESS }; +#else +uint8_t TELEM_BME680_ADDRESSES[] = { 0x76, 0x77 }; // Known I2C addresses for BME680 #endif #define TELEM_BME680_SEALEVELPRESSURE_HPA (1013.25) #include @@ -28,8 +30,10 @@ static Adafruit_AHTX0 AHTX0; #endif #if ENV_INCLUDE_BME280 -#ifndef TELEM_BME280_ADDRESS -#define TELEM_BME280_ADDRESS 0x76 // BME280 environmental sensor I2C address +#if defined(TELEM_BME280_ADDRESS) +uint8_t TELEM_BME280_ADDRESSES[] = { TELEM_BME280_ADDRESS }; +#else +uint8_t TELEM_BME280_ADDRESSES[] = { 0x76, 0x77 }; // Known I2C addresses for BME280 #endif #define TELEM_BME280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level #include @@ -37,8 +41,10 @@ static Adafruit_BME280 BME280; #endif #if ENV_INCLUDE_BMP280 -#ifndef TELEM_BMP280_ADDRESS -#define TELEM_BMP280_ADDRESS 0x76 // BMP280 environmental sensor I2C address +#if defined(TELEM_BMP280_ADDRESS) +uint8_t TELEM_BMP280_ADDRESSES[] = { TELEM_BMP280_ADDRESS }; +#else +uint8_t TELEM_BMP280_ADDRESSES[] = { 0x76, 0x77 }; // Known I2C addresses for BMP280 #endif #define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level #include @@ -161,6 +167,12 @@ class RAK12500LocationProvider : public LocationProvider { static RAK12500LocationProvider RAK12500_provider; #endif +bool EnvironmentSensorManager::i2c_probe(TwoWire &wire, uint8_t addr) { + wire.beginTransmission(addr); + uint8_t error = wire.endTransmission(); + return (error == 0); // If found i2c device, the error is 0 +} + bool EnvironmentSensorManager::begin() { #if ENV_INCLUDE_GPS #ifdef RAK_WISBLOCK_GPS @@ -182,7 +194,7 @@ bool EnvironmentSensorManager::begin() { #endif #if ENV_INCLUDE_AHTX0 - if (AHTX0.begin(TELEM_WIRE, 0, TELEM_AHTX_ADDRESS)) { + if (i2c_probe(*TELEM_WIRE, TELEM_AHTX_ADDRESS) && AHTX0.begin(TELEM_WIRE, 0, TELEM_AHTX_ADDRESS)) { MESH_DEBUG_PRINTLN("Found AHT10/AHT20 at address: %02X", TELEM_AHTX_ADDRESS); AHTX0_initialized = true; } else { @@ -192,46 +204,55 @@ bool EnvironmentSensorManager::begin() { #endif #if ENV_INCLUDE_BME680 - if (BME680.begin(TELEM_BME680_ADDRESS)) { - MESH_DEBUG_PRINTLN("Found BME680 at address: %02X", TELEM_BME680_ADDRESS); - BME680_initialized = true; - } else { - BME680_initialized = false; - MESH_DEBUG_PRINTLN("BME680 was not found at I2C address %02X", TELEM_BME680_ADDRESS); + for (size_t i = 0; i < sizeof(TELEM_BME680_ADDRESSES) / sizeof(TELEM_BME680_ADDRESSES[0]); i++) { + if (i2c_probe(*TELEM_WIRE, TELEM_BME680_ADDRESSES[i]) && BME680.begin(TELEM_BME680_ADDRESSES[i], TELEM_WIRE)) { + MESH_DEBUG_PRINTLN("Found BME680 at address: %02X", TELEM_BME680_ADDRESSES[i]); + BME680_initialized = true; + break; + } else { + BME680_initialized = false; + MESH_DEBUG_PRINTLN("BME680 was not found at I2C address %02X", TELEM_BME680_ADDRESSES[i]); + } } #endif #if ENV_INCLUDE_BME280 - if (BME280.begin(TELEM_BME280_ADDRESS, TELEM_WIRE)) { - MESH_DEBUG_PRINTLN("Found BME280 at address: %02X", TELEM_BME280_ADDRESS); - MESH_DEBUG_PRINTLN("BME sensor ID: %02X", BME280.sensorID()); - // Reduce self-heating: single-shot conversions, light oversampling, long standby. - BME280.setSampling(Adafruit_BME280::MODE_FORCED, + for (size_t i = 0; i < sizeof(TELEM_BME280_ADDRESSES) / sizeof(TELEM_BME280_ADDRESSES[0]); i++) { + if (i2c_probe(*TELEM_WIRE, TELEM_BME280_ADDRESSES[i]) && BME280.begin(TELEM_BME280_ADDRESSES[i], TELEM_WIRE)) { + MESH_DEBUG_PRINTLN("Found BME280 at address: %02X", TELEM_BME280_ADDRESSES[i]); + MESH_DEBUG_PRINTLN("BME sensor ID: %02X", BME280.sensorID()); + // Reduce self-heating: single-shot conversions, light oversampling, long standby. + BME280.setSampling(Adafruit_BME280::MODE_FORCED, Adafruit_BME280::SAMPLING_X1, // temperature Adafruit_BME280::SAMPLING_X1, // pressure Adafruit_BME280::SAMPLING_X1, // humidity Adafruit_BME280::FILTER_OFF, Adafruit_BME280::STANDBY_MS_1000); - BME280_initialized = true; - } else { - BME280_initialized = false; - MESH_DEBUG_PRINTLN("BME280 was not found at I2C address %02X", TELEM_BME280_ADDRESS); + BME280_initialized = true; + break; + } else { + BME280_initialized = false; + MESH_DEBUG_PRINTLN("BME280 was not found at I2C address %02X", TELEM_BME280_ADDRESSES[i]); + } } - #endif +#endif #if ENV_INCLUDE_BMP280 - if (BMP280.begin(TELEM_BMP280_ADDRESS)) { - MESH_DEBUG_PRINTLN("Found BMP280 at address: %02X", TELEM_BMP280_ADDRESS); - MESH_DEBUG_PRINTLN("BMP sensor ID: %02X", BMP280.sensorID()); - BMP280_initialized = true; - } else { - BMP280_initialized = false; - MESH_DEBUG_PRINTLN("BMP280 was not found at I2C address %02X", TELEM_BMP280_ADDRESS); - } - #endif + for (size_t i = 0; i < sizeof(TELEM_BMP280_ADDRESSES) / sizeof(TELEM_BMP280_ADDRESSES[0]); i++) { + if (i2c_probe(*TELEM_WIRE, TELEM_BMP280_ADDRESSES[i]) && BMP280.begin(TELEM_BMP280_ADDRESSES[i])) { + MESH_DEBUG_PRINTLN("Found BMP280 at address: %02X", TELEM_BMP280_ADDRESSES[i]); + MESH_DEBUG_PRINTLN("BMP sensor ID: %02X", BMP280.sensorID()); + BMP280_initialized = true; + break; + } else { + BMP280_initialized = false; + MESH_DEBUG_PRINTLN("BMP280 was not found at I2C address %02X", TELEM_BMP280_ADDRESSES[i]); + } + } +#endif #if ENV_INCLUDE_SHTC3 - if (SHTC3.begin(TELEM_WIRE)) { + if (i2c_probe(*TELEM_WIRE, 0x70) && SHTC3.begin(TELEM_WIRE)) { MESH_DEBUG_PRINTLN("Found sensor: SHTC3"); SHTC3_initialized = true; } else { @@ -242,21 +263,23 @@ bool EnvironmentSensorManager::begin() { #if ENV_INCLUDE_SHT4X - SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS); - uint32_t serialNumber = 0; - int16_t sht4x_error; - sht4x_error = SHT4X.serialNumber(serialNumber); - if (sht4x_error == 0) { - MESH_DEBUG_PRINTLN("Found SHT4X at address: %02X", TELEM_SHT4X_ADDRESS); - SHT4X_initialized = true; - } else { - SHT4X_initialized = false; - MESH_DEBUG_PRINTLN("SHT4X was not found at I2C address %02X", TELEM_SHT4X_ADDRESS); + if (i2c_probe(*TELEM_WIRE, TELEM_SHT4X_ADDRESS)) { + SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS); + uint32_t serialNumber = 0; + int16_t sht4x_error; + sht4x_error = SHT4X.serialNumber(serialNumber); + if (sht4x_error == 0) { + MESH_DEBUG_PRINTLN("Found SHT4X at address: %02X", TELEM_SHT4X_ADDRESS); + SHT4X_initialized = true; + } else { + SHT4X_initialized = false; + MESH_DEBUG_PRINTLN("SHT4X was not found at I2C address %02X", TELEM_SHT4X_ADDRESS); + } } #endif #if ENV_INCLUDE_LPS22HB - if (LPS22HB.begin()) { + if (i2c_probe(*TELEM_WIRE, 0x5C) && LPS22HB.begin()) { MESH_DEBUG_PRINTLN("Found sensor: LPS22HB"); LPS22HB_initialized = true; } else { @@ -266,7 +289,7 @@ bool EnvironmentSensorManager::begin() { #endif #if ENV_INCLUDE_INA3221 - if (INA3221.begin(TELEM_INA3221_ADDRESS, TELEM_WIRE)) { + if (i2c_probe(*TELEM_WIRE, TELEM_INA3221_ADDRESS) && INA3221.begin(TELEM_INA3221_ADDRESS, TELEM_WIRE)) { MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS); MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID()); @@ -281,7 +304,7 @@ bool EnvironmentSensorManager::begin() { #endif #if ENV_INCLUDE_INA219 - if (INA219.begin(TELEM_WIRE)) { + if (i2c_probe(*TELEM_WIRE, TELEM_INA219_ADDRESS) && INA219.begin(TELEM_WIRE)) { MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS); INA219_initialized = true; } else { @@ -291,17 +314,17 @@ bool EnvironmentSensorManager::begin() { #endif #if ENV_INCLUDE_INA260 - if (INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) { + if (i2c_probe(*TELEM_WIRE, TELEM_INA260_ADDRESS) && INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) { MESH_DEBUG_PRINTLN("Found INA260 at address: %02X", TELEM_INA260_ADDRESS); INA260_initialized = true; } else { INA260_initialized = false; - MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA260_ADDRESS); + MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA219_ADDRESS); } #endif #if ENV_INCLUDE_INA226 - if (INA226.begin()) { + if (i2c_probe(*TELEM_WIRE, TELEM_INA226_ADDRESS) && INA226.begin()) { MESH_DEBUG_PRINTLN("Found INA226 at address: %02X", TELEM_INA226_ADDRESS); INA226.setMaxCurrentShunt(TELEM_INA226_MAX_AMP, TELEM_INA226_SHUNT_VALUE); INA226_initialized = true; @@ -312,7 +335,7 @@ bool EnvironmentSensorManager::begin() { #endif #if ENV_INCLUDE_MLX90614 - if (MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) { + if (i2c_probe(*TELEM_WIRE, TELEM_MLX90614_ADDRESS) && MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) { MESH_DEBUG_PRINTLN("Found MLX90614 at address: %02X", TELEM_MLX90614_ADDRESS); MLX90614_initialized = true; } else { @@ -322,7 +345,7 @@ bool EnvironmentSensorManager::begin() { #endif #if ENV_INCLUDE_VL53L0X - if (VL53L0X.begin(TELEM_VL53L0X_ADDRESS, false, TELEM_WIRE)) { + if (i2c_probe(*TELEM_WIRE, TELEM_VL53L0X_ADDRESS) && VL53L0X.begin(TELEM_VL53L0X_ADDRESS, false, TELEM_WIRE)) { MESH_DEBUG_PRINTLN("Found VL53L0X at address: %02X", TELEM_VL53L0X_ADDRESS); VL53L0X_initialized = true; } else { @@ -334,7 +357,7 @@ bool EnvironmentSensorManager::begin() { #if ENV_INCLUDE_BMP085 // First argument is MODE (aka oversampling) // choose ULTRALOWPOWER - if (BMP085.begin(0, TELEM_WIRE)) { + if (i2c_probe(*TELEM_WIRE, 0x77) && BMP085.begin(0, TELEM_WIRE)) { MESH_DEBUG_PRINTLN("Found sensor BMP085"); BMP085_initialized = true; } else { @@ -345,7 +368,7 @@ bool EnvironmentSensorManager::begin() { #if ENV_INCLUDE_RAK12035 RAK12035.setup(*TELEM_WIRE); - if (RAK12035.begin(TELEM_RAK12035_ADDRESS)) { + if (i2c_probe(*TELEM_WIRE, TELEM_RAK12035_ADDRESS) && RAK12035.begin(TELEM_RAK12035_ADDRESS)) { MESH_DEBUG_PRINTLN("Found sensor RAK12035 at address: %02X", TELEM_RAK12035_ADDRESS); RAK12035_initialized = true; } else { @@ -716,7 +739,7 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ gps_detected = true; return true; } - + pinMode(ioPin, INPUT); MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next"); return false; @@ -759,7 +782,7 @@ void EnvironmentSensorManager::loop() { #if ENV_INCLUDE_GPS if (gps_active) { - _location->loop(); + _location->loop(); } if (millis() > next_gps_update) { diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h index 32413ebc03..38de556068 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -47,6 +47,7 @@ class EnvironmentSensorManager : public SensorManager { #else EnvironmentSensorManager(){}; #endif + bool i2c_probe(TwoWire& wire, uint8_t addr) override; bool begin() override; bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override; #if ENV_INCLUDE_GPS diff --git a/variants/gat562_30s_mesh_kit/platformio.ini b/variants/gat562_30s_mesh_kit/platformio.ini index 1467f0fa3d..4266d1346b 100644 --- a/variants/gat562_30s_mesh_kit/platformio.ini +++ b/variants/gat562_30s_mesh_kit/platformio.ini @@ -7,7 +7,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/gat562_30s_mesh_kit -D RAK_4631 -D RAK_BOARD - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/gat562_mesh_evb_pro/platformio.ini b/variants/gat562_mesh_evb_pro/platformio.ini index cede9c97c0..f098237f99 100644 --- a/variants/gat562_mesh_evb_pro/platformio.ini +++ b/variants/gat562_mesh_evb_pro/platformio.ini @@ -5,7 +5,7 @@ board_check = true build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_evb_pro - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D RADIO_CLASS=CustomSX1262 diff --git a/variants/gat562_mesh_tracker_pro/platformio.ini b/variants/gat562_mesh_tracker_pro/platformio.ini index 8a947bce74..cf25424b72 100644 --- a/variants/gat562_mesh_tracker_pro/platformio.ini +++ b/variants/gat562_mesh_tracker_pro/platformio.ini @@ -5,7 +5,7 @@ board_check = true build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_tracker_pro - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/gat562_mesh_watch13/platformio.ini b/variants/gat562_mesh_watch13/platformio.ini index ef30829d5c..f34f0167fd 100644 --- a/variants/gat562_mesh_watch13/platformio.ini +++ b/variants/gat562_mesh_watch13/platformio.ini @@ -8,7 +8,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/gat562_mesh_watch13 -D RAK_4631 -D RAK_BOARD - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/heltec_t096/LoRaFEMControl.h b/variants/heltec_t096/LoRaFEMControl.h index 2c50b74289..a3b5c4ed9b 100644 --- a/variants/heltec_t096/LoRaFEMControl.h +++ b/variants/heltec_t096/LoRaFEMControl.h @@ -12,10 +12,11 @@ class LoRaFEMControl void setRxModeEnable(void); void setRxModeEnableWhenMCUSleep(void); void setLNAEnable(bool enabled); - bool isLnaCanControl(void) { return lna_can_control; } + bool isLnaCanControl(void) const { return lna_can_control; } void setLnaCanControl(bool can_control) { lna_can_control = can_control; } + bool isLNAEnabled(void) const { return lna_enabled; } private: - bool lna_enabled = false; + bool lna_enabled = true; bool lna_can_control = false; }; diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 550131571f..54425145c4 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -123,4 +123,22 @@ void T096Board::powerOff() { const char* T096Board::getManufacturerName() const { return "Heltec T096"; -} \ No newline at end of file +} + +bool T096Board::setLoRaFemLnaEnabled(bool enable) { + if (!loRaFEMControl.isLnaCanControl()) { + return false; + } + + loRaFEMControl.setLNAEnable(enable); + loRaFEMControl.setRxModeEnable(); + return true; +} + +bool T096Board::canControlLoRaFemLna() const { + return loRaFEMControl.isLnaCanControl(); +} + +bool T096Board::isLoRaFemLnaEnabled() const { + return loRaFEMControl.isLNAEnabled(); +} diff --git a/variants/heltec_t096/T096Board.h b/variants/heltec_t096/T096Board.h index d1e3bdfdee..15c7e68b5d 100644 --- a/variants/heltec_t096/T096Board.h +++ b/variants/heltec_t096/T096Board.h @@ -25,4 +25,7 @@ class T096Board : public NRF52BoardDCDC { uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; void powerOff() override; + bool setLoRaFemLnaEnabled(bool enable) override; + bool canControlLoRaFemLna() const override; + bool isLoRaFemLnaEnabled() const override; }; diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index 19b05f3ce4..00cc5d1069 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/heltec_t096 -I src/helpers/ui -D HELTEC_T096 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=21 -D P_LORA_NSS=5 -D P_LORA_RESET=16 @@ -126,6 +126,30 @@ lib_deps = ${Heltec_t096.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_t096_companion_radio_ble_femoff] +extends = Heltec_t096 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${Heltec_t096.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D ENV_INCLUDE_GPS=1 ; enable the GPS page in UI +; -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off +build_src_filter = ${Heltec_t096.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_t096.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Heltec_t096_companion_radio_usb] extends = Heltec_t096 board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index b985030f79..8011641dbe 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -12,7 +12,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/heltec_t114 -I src/helpers/ui -D HELTEC_T114 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=20 -D P_LORA_NSS=24 -D P_LORA_RESET=25 diff --git a/variants/heltec_tracker/platformio.ini b/variants/heltec_tracker/platformio.ini index e0a8f5fab6..2c9155269b 100644 --- a/variants/heltec_tracker/platformio.ini +++ b/variants/heltec_tracker/platformio.ini @@ -99,6 +99,33 @@ lib_deps = ${Heltec_tracker_base.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_Wireless_Tracker_companion_radio_ble_ps] +extends = Heltec_tracker_base +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${Heltec_tracker_base.build_flags} + -I src/helpers/ui + -I examples/companion_radio/ui-new + -D DISPLAY_ROTATION=1 + -D DISPLAY_CLASS=ST7735Display + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 ; HWT will use display for pin + -D OFFLINE_QUEUE_SIZE=256 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_tracker_base.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> + + +lib_deps = + ${Heltec_tracker_base.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Heltec_Wireless_Tracker_repeater] extends = Heltec_tracker_base build_flags = diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp index aabfed7967..f182c905e6 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp @@ -82,3 +82,21 @@ void HeltecTrackerV2Board::begin() { const char* HeltecTrackerV2Board::getManufacturerName() const { return "Heltec Tracker V2"; } + + bool HeltecTrackerV2Board::setLoRaFemLnaEnabled(bool enable) { + if (!loRaFEMControl.isLnaCanControl()) { + return false; + } + + loRaFEMControl.setLNAEnable(enable); + loRaFEMControl.setRxModeEnable(); + return true; + } + + bool HeltecTrackerV2Board::canControlLoRaFemLna() const { + return loRaFEMControl.isLnaCanControl(); + } + + bool HeltecTrackerV2Board::isLoRaFemLnaEnabled() const { + return loRaFEMControl.isLNAEnabled(); + } diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h index 33c897bc94..ccbecc7ab6 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h @@ -21,5 +21,8 @@ class HeltecTrackerV2Board : public ESP32Board { void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; + bool setLoRaFemLnaEnabled(bool enable) override; + bool canControlLoRaFemLna() const override; + bool isLoRaFemLnaEnabled() const override; }; diff --git a/variants/heltec_tracker_v2/LoRaFEMControl.h b/variants/heltec_tracker_v2/LoRaFEMControl.h index 2c50b74289..a3b5c4ed9b 100644 --- a/variants/heltec_tracker_v2/LoRaFEMControl.h +++ b/variants/heltec_tracker_v2/LoRaFEMControl.h @@ -12,10 +12,11 @@ class LoRaFEMControl void setRxModeEnable(void); void setRxModeEnableWhenMCUSleep(void); void setLNAEnable(bool enabled); - bool isLnaCanControl(void) { return lna_can_control; } + bool isLnaCanControl(void) const { return lna_can_control; } void setLnaCanControl(bool can_control) { lna_can_control = can_control; } + bool isLNAEnabled(void) const { return lna_enabled; } private: - bool lna_enabled = false; + bool lna_enabled = true; bool lna_can_control = false; }; diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index 956b1ec771..f7b87133b5 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -176,6 +176,33 @@ lib_deps = ${Heltec_tracker_v2.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:heltec_tracker_v2_companion_radio_ble_ps] +extends = Heltec_tracker_v2 +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${Heltec_tracker_v2.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=ST7735Display + -D BLE_PIN_CODE=123456 ; dynamic, random PIN + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 + ; -D BLE_DEBUG_LOGGING=1 + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_tracker_v2.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_tracker_v2.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:heltec_tracker_v2_companion_radio_wifi] extends = Heltec_tracker_v2 build_flags = diff --git a/variants/heltec_v2/HeltecV2Board.h b/variants/heltec_v2/HeltecV2Board.h index a6221036dd..fe800890b8 100644 --- a/variants/heltec_v2/HeltecV2Board.h +++ b/variants/heltec_v2/HeltecV2Board.h @@ -17,12 +17,12 @@ class HeltecV2Board : public ESP32Board { esp_reset_reason_t reason = esp_reset_reason(); if (reason == ESP_RST_DEEPSLEEP) { long wakeup_source = esp_sleep_get_ext1_wakeup_status(); - if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep) + if (wakeup_source & (1 << P_LORA_DIO_0)) { // received a LoRa packet (while in deep sleep) startup_reason = BD_STARTUP_RX_PACKET; } rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS); - rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); + rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_0); } } @@ -30,15 +30,15 @@ class HeltecV2Board : public ESP32Board { esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_0, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_0); rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_0), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_0) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn } if (secs > 0) { @@ -64,4 +64,8 @@ class HeltecV2Board : public ESP32Board { const char* getManufacturerName() const override { return "Heltec V2"; } + + uint32_t getIRQGpio() override { + return P_LORA_DIO_0; // default for SX1276 + } }; diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index 99f6f7e13c..2ff6e9ff6b 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -7,10 +7,10 @@ build_flags = -D HELTEC_LORA_V2 -D RADIO_CLASS=CustomSX1276 -D WRAPPER_CLASS=CustomSX1276Wrapper - -D P_LORA_DIO_1=26 + -D P_LORA_DIO_0=26 + -D P_LORA_DIO_1=35 -D P_LORA_NSS=18 - -D P_LORA_RESET=RADIOLIB_NC - -D P_LORA_BUSY=RADIOLIB_NC + -D P_LORA_RESET=14 -D P_LORA_SCLK=5 -D P_LORA_MISO=19 -D P_LORA_MOSI=27 @@ -172,6 +172,31 @@ lib_deps = ${Heltec_lora32_v2.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_v2_companion_radio_ble_ps] +extends = Heltec_lora32_v2 +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${Heltec_lora32_v2.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=SSD1306Display + -D MAX_CONTACTS=160 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_lora32_v2.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_lora32_v2.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Heltec_v2_companion_radio_wifi] extends = Heltec_lora32_v2 build_flags = diff --git a/variants/heltec_v2/target.cpp b/variants/heltec_v2/target.cpp index 2dfb4c6e1a..54a2d7b278 100644 --- a/variants/heltec_v2/target.cpp +++ b/variants/heltec_v2/target.cpp @@ -5,9 +5,9 @@ HeltecV2Board board; #if defined(P_LORA_SCLK) static SPIClass spi; - RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi); #else - RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1); #endif WRAPPER_CLASS radio_driver(radio, board); diff --git a/variants/heltec_v3/HeltecV3Board.h b/variants/heltec_v3/HeltecV3Board.h index ba22a7f2b9..5361d0293d 100644 --- a/variants/heltec_v3/HeltecV3Board.h +++ b/variants/heltec_v3/HeltecV3Board.h @@ -53,6 +53,10 @@ class HeltecV3Board : public ESP32Board { } void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { + // Clear stale wakeup sources to avoid ghost wakeup + // This is required when Power Management and automatic lightsleep are enabled + esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 803ee683e0..fe9cd9264b 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -179,6 +179,32 @@ lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_v3_companion_radio_ble_ps] +extends = Heltec_lora32_v3 +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${Heltec_lora32_v3.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display + -D BLE_PIN_CODE=123456 ; dynamic, random PIN + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_lora32_v3.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Heltec_v3_companion_radio_wifi] extends = Heltec_lora32_v3 build_flags = @@ -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} + + + +<../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 = diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index 49580d2ecf..87791e6b6f 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -33,6 +33,10 @@ void HeltecV4Board::begin() { } void HeltecV4Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + // Clear stale wakeup sources to avoid ghost wakeup + // This is required when Power Management and automatic lightsleep are enabled + esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep @@ -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(); + } diff --git a/variants/heltec_v4/HeltecV4Board.h b/variants/heltec_v4/HeltecV4Board.h index 4d5ee46155..f96e161c8d 100644 --- a/variants/heltec_v4/HeltecV4Board.h +++ b/variants/heltec_v4/HeltecV4Board.h @@ -17,6 +17,9 @@ class HeltecV4Board : public ESP32Board { 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 ; diff --git a/variants/heltec_v4/LoRaFEMControl.h b/variants/heltec_v4/LoRaFEMControl.h index 7545296503..d84ebe9c6a 100644 --- a/variants/heltec_v4/LoRaFEMControl.h +++ b/variants/heltec_v4/LoRaFEMControl.h @@ -18,8 +18,9 @@ class LoRaFEMControl void setRxModeEnable(void); void setRxModeEnableWhenMCUSleep(void); void setLNAEnable(bool enabled); - bool isLnaCanControl(void) { return lna_can_control; } + bool isLnaCanControl(void) const { return lna_can_control; } void setLnaCanControl(bool can_control) { lna_can_control = can_control; } + bool isLNAEnabled(void) const { return lna_enabled; } LoRaFEMType getFEMType(void) const { return fem_type; } private: LoRaFEMType fem_type=OTHER_FEM_TYPES; diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index 6f6bf2b538..bc038c8e4c 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -102,6 +102,28 @@ lib_deps = ${esp32_ota.lib_deps} bakercp/CRC32 @ ^2.0.0 +[env:heltec_v4_expansionkit_repeater] +extends = heltec_v4_oled +build_flags = + ${heltec_v4_oled.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"Heltec Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D ENV_PIN_SDA=4 + -D ENV_PIN_SCL=3 +build_src_filter = ${heltec_v4_oled.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${heltec_v4_oled.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + [env:heltec_v4_repeater_bridge_espnow] extends = heltec_v4_oled build_flags = @@ -200,14 +222,66 @@ lib_deps = ${heltec_v4_oled.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:heltec_v4_companion_radio_wifi] +[env:heltec_v4_companion_radio_ble_ps] +extends = heltec_v4_oled +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${heltec_v4_oled.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display + -D BLE_PIN_CODE=123456 ; dynamic, random PIN + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${heltec_v4_oled.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_oled.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_3_companion_radio_ble_ps_femoff] extends = heltec_v4_oled +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 build_flags = ${heltec_v4_oled.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display + -D BLE_PIN_CODE=123456 ; dynamic, random PIN + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off +build_src_filter = ${heltec_v4_oled.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_oled.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_companion_radio_wifi] +extends = heltec_v4_oled +build_flags = + ${heltec_v4_oled.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display -D WIFI_DEBUG_LOGGING=1 -D WIFI_SSID='"myssid"' diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index c6fe657d47..2e4c4c9dd5 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -64,6 +64,29 @@ lib_deps = densaugeo/base64 @ ~1.4.0 bakercp/CRC32 @ ^2.0.0 +[env:Heltec_Wireless_Paper_companion_radio_ble_ps] +extends = Heltec_Wireless_Paper_base +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${Heltec_Wireless_Paper_base.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=E213Display + -D BLE_PIN_CODE=123456 ; dynamic, random PIN + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${Heltec_Wireless_Paper_base.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_Wireless_Paper_base.lib_deps} + densaugeo/base64 @ ~1.4.0 + bakercp/CRC32 @ ^2.0.0 + [env:Heltec_Wireless_Paper_companion_radio_usb] extends = Heltec_Wireless_Paper_base build_flags = diff --git a/variants/lilygo_t3s3_sx1276/LilygoT3S3SX1276Board.h b/variants/lilygo_t3s3_sx1276/LilygoT3S3SX1276Board.h new file mode 100644 index 0000000000..7da620fd50 --- /dev/null +++ b/variants/lilygo_t3s3_sx1276/LilygoT3S3SX1276Board.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +class LilygoT3S3SX1276Board : public ESP32Board { +public: + uint32_t getIRQGpio() override { + return P_LORA_DIO_0; // default for SX1276 + } +}; \ No newline at end of file diff --git a/variants/lilygo_t3s3_sx1276/target.cpp b/variants/lilygo_t3s3_sx1276/target.cpp index e7fe07a0c4..8236f45960 100644 --- a/variants/lilygo_t3s3_sx1276/target.cpp +++ b/variants/lilygo_t3s3_sx1276/target.cpp @@ -1,7 +1,7 @@ #include #include "target.h" -ESP32Board board; +LilygoT3S3SX1276Board board; #if defined(P_LORA_SCLK) static SPIClass spi; diff --git a/variants/lilygo_t3s3_sx1276/target.h b/variants/lilygo_t3s3_sx1276/target.h index 2df4b3edb5..079d2a7ea2 100644 --- a/variants/lilygo_t3s3_sx1276/target.h +++ b/variants/lilygo_t3s3_sx1276/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include +#include #include #include #include @@ -12,7 +12,7 @@ #include #endif -extern ESP32Board board; +extern LilygoT3S3SX1276Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; extern SensorManager sensors; diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index 7c8453077f..60b3291b74 100644 --- a/variants/lilygo_tbeam_1w/platformio.ini +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -146,6 +146,31 @@ lib_deps = ${LilyGo_TBeam_1W.lib_deps} densaugeo/base64 @ ~1.4.0 +; === LILYGO T-Beam 1W Companion Radio PS (BLE PS) === +[env:LilyGo_TBeam_1W_companion_radio_ble_ps] +extends = LilyGo_TBeam_1W +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${LilyGo_TBeam_1W.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 + -D PERSISTANT_GPS=1 + -D ENV_SKIP_GPS_DETECT=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TBeam_1W.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_TBeam_1W.lib_deps} + densaugeo/base64 @ ~1.4.0 + ; === LILYGO T-Beam 1W Companion Radio (WiFi) === [env:LilyGo_TBeam_1W_companion_radio_wifi] extends = LilyGo_TBeam_1W diff --git a/variants/lilygo_tbeam_SX1262/platformio.ini b/variants/lilygo_tbeam_SX1262/platformio.ini index d3bc7c9978..1585dd74d3 100644 --- a/variants/lilygo_tbeam_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_SX1262/platformio.ini @@ -5,6 +5,13 @@ build_flags = ${esp32_base.build_flags} -I variants/lilygo_tbeam_SX1262 -D TBEAM_SX1262 + -D P_LORA_DIO_0=26 + -D P_LORA_DIO_1=33 + -D P_LORA_NSS=18 + -D P_LORA_RESET=23 + -D P_LORA_SCLK=5 + -D P_LORA_MISO=19 + -D P_LORA_MOSI=27 -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 diff --git a/variants/lilygo_tbeam_SX1276/LilygoTBeamSX1276Board.h b/variants/lilygo_tbeam_SX1276/LilygoTBeamSX1276Board.h new file mode 100644 index 0000000000..afe106d042 --- /dev/null +++ b/variants/lilygo_tbeam_SX1276/LilygoTBeamSX1276Board.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +class LilygoTBeamSX1276Board : public TBeamBoard { +public: + uint32_t getIRQGpio() override { + return P_LORA_DIO_0; // default for SX1276 + } +}; \ No newline at end of file diff --git a/variants/lilygo_tbeam_SX1276/platformio.ini b/variants/lilygo_tbeam_SX1276/platformio.ini index 3562c40e94..7482ef7bd8 100644 --- a/variants/lilygo_tbeam_SX1276/platformio.ini +++ b/variants/lilygo_tbeam_SX1276/platformio.ini @@ -5,6 +5,13 @@ build_flags = ${esp32_base.build_flags} -I variants/lilygo_tbeam_SX1276 -D TBEAM_SX1276 + -D P_LORA_DIO_0=26 + -D P_LORA_DIO_1=33 + -D P_LORA_NSS=18 + -D P_LORA_RESET=23 + -D P_LORA_SCLK=5 + -D P_LORA_MISO=19 + -D P_LORA_MOSI=27 -D SX127X_CURRENT_LIMIT=120 -D RADIO_CLASS=CustomSX1276 -D WRAPPER_CLASS=CustomSX1276Wrapper diff --git a/variants/lilygo_tbeam_SX1276/target.cpp b/variants/lilygo_tbeam_SX1276/target.cpp index 495337b8e2..5481d67240 100644 --- a/variants/lilygo_tbeam_SX1276/target.cpp +++ b/variants/lilygo_tbeam_SX1276/target.cpp @@ -1,7 +1,7 @@ #include #include "target.h" -TBeamBoard board; +LilygoTBeamSX1276Board board; #if defined(P_LORA_SCLK) static SPIClass spi; diff --git a/variants/lilygo_tbeam_SX1276/target.h b/variants/lilygo_tbeam_SX1276/target.h index ad3856455e..abeaef4698 100644 --- a/variants/lilygo_tbeam_SX1276/target.h +++ b/variants/lilygo_tbeam_SX1276/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 //#include #include -#include +#include #include #include #include @@ -12,7 +12,7 @@ #include #endif -extern TBeamBoard board; +extern LilygoTBeamSX1276Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; extern EnvironmentSensorManager sensors; diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index ffee37a969..249e68713b 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -5,6 +5,13 @@ build_flags = ${esp32_base.build_flags} -I variants/lilygo_tbeam_supreme_SX1262 -D TBEAM_SUPREME_SX1262 + -D P_LORA_DIO_0=26 + -D P_LORA_DIO_1=33 + -D P_LORA_NSS=18 + -D P_LORA_RESET=23 + -D P_LORA_SCLK=5 + -D P_LORA_MISO=19 + -D P_LORA_MOSI=27 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 -D USE_SX1262 diff --git a/variants/lilygo_tlora_v2_1/LilyGoTLoraBoard.h b/variants/lilygo_tlora_v2_1/LilyGoTLoraBoard.h index 545219b2bc..f126f00688 100644 --- a/variants/lilygo_tlora_v2_1/LilyGoTLoraBoard.h +++ b/variants/lilygo_tlora_v2_1/LilyGoTLoraBoard.h @@ -21,4 +21,8 @@ class LilyGoTLoraBoard : public ESP32Board { return (2 * raw); } + + uint32_t getIRQGpio() override { + return P_LORA_DIO_0; // default for SX1276 + } }; \ No newline at end of file diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 3d2d4a3ec5..e12071149f 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -6,7 +6,7 @@ build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak3401 -D RAK_3401 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index ea7e49c355..a96374a8de 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -7,7 +7,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/rak4631 -D RAK_4631 -D RAK_BOARD - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_GPS_TX=PIN_SERIAL1_RX diff --git a/variants/sensecap_solar/platformio.ini b/variants/sensecap_solar/platformio.ini index aabbcf000b..6aeb509b3a 100644 --- a/variants/sensecap_solar/platformio.ini +++ b/variants/sensecap_solar/platformio.ini @@ -10,7 +10,7 @@ build_flags = ${nrf52_base.build_flags} -I src/helpers/nrf52 -D NRF52_PLATFORM=1 -D USE_SX1262 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D P_LORA_TX_LED=11 diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index c5254b46c9..244a39f157 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -90,6 +90,28 @@ lib_deps = ${esp32_ota.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Xiao_C3_companion_radio_ble_ps] +extends = Xiao_esp32_C3 +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_src_filter = ${Xiao_esp32_C3.build_src_filter} + +<../examples/companion_radio/*.cpp> + + +build_flags = + ${Xiao_esp32_C3.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 + ; -D BLE_DEBUG_LOGGING=1 + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +lib_deps = + ${Xiao_esp32_C3.lib_deps} + ${esp32_ota.lib_deps} + densaugeo/base64 @ ~1.4.0 +board_build.partitions = min_spiffs.csv ; get around 4mb flash limit + [env:Xiao_C3_companion_radio_usb] extends = Xiao_esp32_C3 build_src_filter = ${Xiao_esp32_C3.build_src_filter} diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index a085433688..f4d1b93e6b 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/xiao_nrf52 -UENV_INCLUDE_GPS -D NRF52_PLATFORM - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D XIAO_NRF52 -D USE_SX1262 -D RADIO_CLASS=CustomSX1262 diff --git a/variants/xiao_s3/XiaoS3Board.h b/variants/xiao_s3/XiaoS3Board.h new file mode 100644 index 0000000000..288fcf6262 --- /dev/null +++ b/variants/xiao_s3/XiaoS3Board.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +class XiaoS3Board : public ESP32Board { +public: + XiaoS3Board() { } + + const char* getManufacturerName() const override { + return "Xiao S3"; + } +}; diff --git a/variants/xiao_s3/platformio.ini b/variants/xiao_s3/platformio.ini new file mode 100644 index 0000000000..58bcc60b90 --- /dev/null +++ b/variants/xiao_s3/platformio.ini @@ -0,0 +1,180 @@ +[Xiao_S3] +extends = esp32_base +board = seeed_xiao_esp32s3 +board_check = true +board_build.mcu = esp32s3 +build_flags = ${esp32_base.build_flags} + ${sensor_base.build_flags} + -I variants/xiao_s3 + -UENV_INCLUDE_GPS + -D SEEED_XIAO_S3 + -D PIN_VBAT_READ=1 ; D0 + -D P_LORA_DIO_1=2 ; D1 + -D P_LORA_NSS=5 ; D4 + -D P_LORA_RESET=3 ; D2 + -D P_LORA_BUSY=4 ; D3 + -D P_LORA_SCLK=7 ; D8 + -D P_LORA_MISO=8 ; D0 + -D P_LORA_MOSI=9 ; D10 + -D PIN_USER_BTN=-1 ; NC + -D PIN_STATUS_LED=21 ; Orange user led, LOW=On + -D PIN_BOARD_SDA=D6 ; D6=43 + -D PIN_BOARD_SCL=D7 ; D7=44 + -D SX126X_RXEN=6 ; D5 + -D SX126X_TXEN=RADIOLIB_NC + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_RX_BOOSTED_GAIN=1 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/xiao_s3> + + +lib_deps = + ${esp32_base.lib_deps} + ${sensor_base.lib_deps} + +[env:Xiao_S3_repeater] +extends = Xiao_S3 +build_src_filter = ${Xiao_S3.build_src_filter} + +<../examples/simple_repeater/*.cpp> +build_flags = + ${Xiao_S3.build_flags} + -D ADVERT_NAME='"Xiao S3 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = + ${Xiao_S3.lib_deps} + ${esp32_ota.lib_deps} + +[env:Xiao_S3_repeater_bridge_espnow] +extends = Xiao_S3 +build_src_filter = ${Xiao_S3.build_src_filter} + + + +<../examples/simple_repeater/*.cpp> +build_flags = + ${Xiao_S3.build_flags} + -D ADVERT_NAME='"ESPNow Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_ESPNOW_BRIDGE=1 +; -D BRIDGE_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = + ${Xiao_S3.lib_deps} + ${esp32_ota.lib_deps} + +[env:Xiao_S3_room_server] +extends = Xiao_S3 +build_src_filter = ${Xiao_S3.build_src_filter} + +<../examples/simple_room_server> +build_flags = + ${Xiao_S3.build_flags} + -D ADVERT_NAME='"Xiao S3 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = + ${Xiao_S3.lib_deps} + ${esp32_ota.lib_deps} + +[env:Xiao_S3_companion_radio_ble] +extends = Xiao_S3 +build_flags = + ${Xiao_S3.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D DISPLAY_CLASS=SSD1306Display + -D OFFLINE_QUEUE_SIZE=256 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Xiao_S3.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Xiao_S3.lib_deps} + densaugeo/base64 @ ~1.4.0 + adafruit/Adafruit SSD1306 @ ^2.5.13 + +[env:Xiao_S3_companion_radio_ble_ps] +extends = Xiao_S3 +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${Xiao_S3.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D DISPLAY_CLASS=SSD1306Display + -D OFFLINE_QUEUE_SIZE=256 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Xiao_S3.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Xiao_S3.lib_deps} + densaugeo/base64 @ ~1.4.0 + adafruit/Adafruit SSD1306 @ ^2.5.13 + +[env:Xiao_S3_companion_radio_usb] +extends = Xiao_S3 +build_flags = + ${Xiao_S3.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display + -D OFFLINE_QUEUE_SIZE=256 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Xiao_S3.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Xiao_S3.lib_deps} + densaugeo/base64 @ ~1.4.0 + adafruit/Adafruit SSD1306 @ ^2.5.13 + +[env:Xiao_S3_sensor] +extends = Xiao_S3 +build_src_filter = ${Xiao_S3.build_src_filter} + +<../examples/simple_sensor> +build_flags = + ${Xiao_S3.build_flags} + -D ADVERT_NAME='"Xiao S3 Sensor"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = + ${Xiao_S3.lib_deps} + ${esp32_ota.lib_deps} \ No newline at end of file diff --git a/variants/xiao_s3/target.cpp b/variants/xiao_s3/target.cpp new file mode 100644 index 0000000000..014b25529d --- /dev/null +++ b/variants/xiao_s3/target.cpp @@ -0,0 +1,56 @@ +#include +#include "target.h" + +XiaoS3Board board; + +#if defined(P_LORA_SCLK) + static SPIClass spi; + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +#else + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +#endif + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + pinMode(21, INPUT); + pinMode(48, OUTPUT); + + #if defined(P_LORA_SCLK) + spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); + return radio.std_init(&spi); +#else + return radio.std_init(); +#endif +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(int8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/xiao_s3/target.h b/variants/xiao_s3/target.h new file mode 100644 index 0000000000..93b2786295 --- /dev/null +++ b/variants/xiao_s3/target.h @@ -0,0 +1,30 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include + #include +#endif +#include "XiaoS3Board.h" + +extern XiaoS3Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(int8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index 13d406792b..7fcebce2eb 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -173,6 +173,32 @@ lib_deps = densaugeo/base64 @ ~1.4.0 adafruit/Adafruit SSD1306 @ ^2.5.13 +[env:Xiao_S3_WIO_companion_radio_ble_ps] +extends = Xiao_S3_WIO +platform_packages = + framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${Xiao_S3_WIO.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D DISPLAY_CLASS=SSD1306Display + -D OFFLINE_QUEUE_SIZE=256 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Xiao_S3_WIO.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Xiao_S3_WIO.lib_deps} + densaugeo/base64 @ ~1.4.0 + adafruit/Adafruit SSD1306 @ ^2.5.13 + [env:Xiao_S3_WIO_companion_radio_serial] extends = Xiao_S3_WIO build_flags =