diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml new file mode 100644 index 0000000000..ec16658756 --- /dev/null +++ b/.github/workflows/stale-bot.yml @@ -0,0 +1,32 @@ +name: 'Run Stale Bot' +on: + schedule: + - cron: '30 1 * * *' # daily at 1:30am + workflow_dispatch: {} + +permissions: + actions: write + issues: write + pull-requests: write + +jobs: + close-issues: + # only run on main repo, not forks + if: github.repository == 'meshcore-dev/MeshCore' + runs-on: ubuntu-latest + steps: + - name: Close Stale Issues + uses: actions/stale@v10 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + # auto close issues + days-before-issue-stale: 60 + days-before-issue-close: 7 + exempt-issue-labels: "keep-open" + stale-issue-label: "stale" + stale-issue-message: "This issue is stale because it has been open for 60 days with no activity. Remove the stale label or add a comment if this issue is still relevant, otherwise this issue will automatically close in 7 days." + close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale." + # don't auto close prs + days-before-pr-stale: -1 + days-before-pr-close: -1 + \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..a4b2207d55 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,57 @@ +# Security Policy + +## Supported Versions + +Security fixes are applied to the latest release only. We do not backport +fixes to older versions. + +| Version | Supported | +|---------|-----------| +| 1.15+ | ✅ | +| <1.15 | ❌ | + +## Reporting a Vulnerability + +**Please do not report security vulnerabilities through public GitHub issues.** + +Use GitHub's private vulnerability reporting instead: +1. Go to the **Security** tab of this repository +2. Click **Report a vulnerability** +3. Fill in the details and submit + +### What to include + +A useful report tells us: +- Which component or file is affected +- What an attacker can do (impact) and under what conditions +- A minimal reproduction case or proof-of-concept if you have one +- Whether you believe it is remotely exploitable + +You do not need a working exploit to report. An incomplete report is better +than no report. + +## What to expect + +This is a volunteer-maintained open-source project. We will do our best to +respond in a reasonable timeframe, but cannot commit to specific deadlines. + +We ask that you give us a fair opportunity to investigate and address the +issue before any public disclosure. If you have not heard back after +**90 days**, feel free to follow up or proceed with disclosure at your +discretion. + +## Scope + +In scope: +- Remote code execution, memory corruption, or denial-of-service via crafted + radio packets +- Authentication or encryption bypasses +- Vulnerabilities in the packet routing or path handling logic + +Out of scope: +- Physical access attacks (e.g., JTAG, UART extraction of keys) +- Regulatory compliance (duty cycle, frequency restrictions) +- Jamming or other physical-layer radio interference +- Issues in third-party libraries (RadioLib, Crypto, etc.) — report those + upstream +- "Best practice" suggestions without a demonstrated attack path diff --git a/build-iotthinks.sh b/build-iotthinks.sh new file mode 100755 index 0000000000..3e41b4d694 --- /dev/null +++ b/build-iotthinks.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# ./build-iotthinks.sh +export FIRMWARE_VERSION="PowerSaving15" + +############# Repeaters ############# +# Commonly-used boards +## ESP32 - 12 boards +./build.sh build-firmware \ +Heltec_v3_repeater \ +Heltec_WSL3_repeater \ +heltec_v4_repeater \ +Station_G2_repeater \ +T_Beam_S3_Supreme_SX1262_repeater \ +Tbeam_SX1262_repeater \ +LilyGo_T3S3_sx1262_repeater \ +Xiao_S3_WIO_repeater \ +Xiao_C3_repeater \ +Xiao_C6_repeater_ \ +Heltec_E290_repeater \ +Heltec_Wireless_Tracker_repeater + +## NRF52 - 13 boards +./build.sh build-firmware \ +RAK_4631_repeater \ +Heltec_t114_repeater \ +Xiao_nrf52_repeater \ +Heltec_mesh_solar_repeater \ +ProMicro_repeater \ +SenseCap_Solar_repeater \ +t1000e_repeater \ +LilyGo_T-Echo_repeater \ +WioTrackerL1_repeater \ +RAK_3401_repeater \ +RAK_WisMesh_Tag_repeater \ +GAT562_30S_Mesh_Kit_repeater \ +GAT562_Mesh_Tracker_Pro_repeater + +## ESP32, SX1276 - 3 boards +./build.sh build-firmware \ +Heltec_v2_repeater \ +LilyGo_TLora_V2_1_1_6_repeater \ +Tbeam_SX1276_repeater + +## Ikoka - 3 boards +./build.sh build-firmware \ +ikoka_nano_nrf_22dbm_repeater \ +ikoka_nano_nrf_30dbm_repeater \ +ikoka_nano_nrf_33dbm_repeater + +############# Room Server ############# +# ESP32 +./build.sh build-firmware \ +Heltec_v3_room_server \ +heltec_v4_room_server + +# NRF52 +./build.sh build-firmware \ +RAK_4631_room_server \ +Heltec_t114_room_server \ +Xiao_nrf52_room_server \ +t1000e_room_server \ +WioTrackerL1_room_server \ +RAK_3401_room_server + +############# Companions BLE ############# +# ESP32 +./build.sh build-firmware \ +Heltec_v3_companion_radio_ble_ps \ +heltec_v4_companion_radio_ble_ps \ +heltec_v4_companion_radio_ble_ps_femoff \ +Xiao_S3_WIO_companion_radio_ble \ +Heltec_Wireless_Paper_companion_radio_ble + +# NRF52 +./build.sh build-firmware \ +RAK_4631_companion_radio_ble \ +Heltec_t114_companion_radio_ble \ +Xiao_nrf52_companion_radio_ble \ +t1000e_companion_radio_ble \ +LilyGo_T-Echo_companion_radio_ble \ +WioTrackerL1_companion_radio_ble \ +RAK_3401_companion_radio_ble \ +RAK_WisMesh_Tag_companion_radio_ble + +############# Companions USB ############# +./build.sh build-firmware \ +Heltec_v3_companion_radio_usb + +############# Companions BLE PS ############# +./build.sh build-firmware \ +Heltec_v3_companion_radio_ble_ps \ +heltec_v4_companion_radio_ble_ps \ +heltec_v4_3_companion_radio_ble_ps_femoff \ +Xiao_C3_companion_radio_ble_ps \ +Xiao_S3_companion_radio_ble_ps \ +Xiao_S3_WIO_companion_radio_ble_ps \ +Heltec_v2_companion_radio_ble_ps 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 c06f5e12b3..ab132014f3 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -28,12 +28,25 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Usage:** - `reboot` +**Note:** No reply is sent. + +--- + +### Power-off the node +**Usage:** +- `poweroff`, or +- `shutdown` + +**Note:** No reply is sent. + --- ### Reset the clock and reboot **Usage:** - `clkreboot` +**Note:** No reply is sent. + --- ### Sync the clock with the remote device @@ -660,10 +673,21 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `value`: Maximum flood hop count (0-64) for a packet without a scope (no region set) -**Default:** `0xFF` - indicates it hasn't been set, will track flood.max until it is. +**Default:** `64` - (`0xFF` indicates it hasn't been set, will track flood.max until it is.) **Note:** An alternative to `region denyf *`, setting `flood.max.unscoped` to a lower value such as `3` would allow for local unscoped messages to propagate, while preventing noisy neighbors from flooding a local region. +--- + +#### Limit the number of hops for an advert flood message +**Usage:** +- `get flood.max.advert` +- `set flood.max.advert ` + +**Parameters:** +- `value`: Maximum flood hop count (0-64) for an advert packet + +**Default:** `8` --- diff --git a/docs/halo_keymind_settings.md b/docs/halo_keymind_settings.md new file mode 100644 index 0000000000..1be3ddb0f8 --- /dev/null +++ b/docs/halo_keymind_settings.md @@ -0,0 +1,290 @@ +# Halo and Keymind Branch Settings + +This file covers only CLI settings added by the Halo or Keymind branches. Use +`docs/cli_commands.md` for the general MeshCore CLI. + +## Quick Start + + +```text +set retry.preset rooftop +set direct.retry.heard on +set flood.retry.advert off +set flood.retry.bridge off +set flood.retry.prefixes none +set flood.retry.ignore none +``` + +Then verify: + +```text +get retry.preset +get direct.retry.heard +get flood.retry.advert +get flood.retry.prefixes +get flood.retry.ignore +``` + +Use prefixes from the analyzer or neighbors list or `get recent.repeater` after the repeater has been online for a few hours. + +## Common Examples + +Disable retrying advert packets: + +```text +set flood.retry.advert off +get flood.retry.advert +``` + +Ignore a repeater as a successful flood retry echo: +Use this if you have a car repeater and a house repeater; have the house ignore the car. + +```text +set flood.retry.ignore 71CE82,C7618C +get flood.retry.ignore +``` + +Only accept specific downstream relays as flood retry success: +You're in a hole and need to hit a mountain top repeater to get out; keep trying till one you see one of these send out your packet. + +```text +set flood.retry.prefixes A58296,860CCA,425E5C +get flood.retry.prefixes +``` + +Bridge two groups of repeaters: + +```text +set flood.retry.bridge on +set flood.retry.bucket 1 71CE82,C7618C +set flood.retry.bucket 2 BEEBB0,425E5C +get flood.retry.bucket.1 +get flood.retry.bucket.2 +``` + +Return to simple non-bridge flood retry: + +```text +set flood.retry.bridge off +set flood.retry.prefixes none +set flood.retry.ignore none +``` + +## Added Settings + +| Setting | What it does | How to use | Example | +| --- | --- | --- | --- | +| `recent.repeater` | Shows or seeds the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. | `get recent.repeater`, `get recent.repeater `, `set recent.repeater ` | `set recent.repeater A1B2C3 -8.5` | +| `radio.fem.rxgain` | Controls the external LoRa FEM receive-path LNA where the board supports it. | `get radio.fem.rxgain`, `set radio.fem.rxgain on/off` | `set radio.fem.rxgain on` | + +## Recent Repeater Table + +Direct retry uses the recent repeater table when `direct.retry.heard` is `on`. +Bridge buckets also use this table: a configured bucket prefix is active only +when it was heard within the last hour. + +Show learned rows: + +```text +get recent.repeater +get recent.repeater 2 +``` + +Seed or correct a prefix: + +```text +set recent.repeater A1B2C3 8.5 +``` + +Rows are sorted by prefix width, then SNR. A full direct retry failure lowers +the matching row by `0.25 dB`. + +## Direct Retry Settings + +Direct retry applies to direct-routed packets. A queued resend is canceled when the next-hop echo is heard. + +| Setting | What it does | How to use | Example | +| --- | --- | --- | --- | +| `retry.preset` | Applies shared direct and flood retry defaults. Values: `infra`, `rooftop`, `mobile` or `0`, `1`, `2`. | `get retry.preset`, `set retry.preset ` | `set retry.preset rooftop` | +| `direct.retry.heard` | Uses the recent repeater table as the direct retry eligibility gate. | `get direct.retry.heard`, `set direct.retry.heard on/off` | `set direct.retry.heard on` | +| `direct.retry.margin` | SNR margin in dB above the SF-specific receive floor. | `get direct.retry.margin`, `set direct.retry.margin <0-40>` | `set direct.retry.margin 5` | +| `direct.retry.count` | Maximum direct retry attempts after initial TX. | `get direct.retry.count`, `set direct.retry.count <1-15>` | `set direct.retry.count 15` | +| `direct.retry.base` | Base wait in milliseconds before retry. | `get direct.retry.base`, `set direct.retry.base <10-5000>` | `set direct.retry.base 175` | +| `direct.retry.step` | Milliseconds added per retry attempt. | `get direct.retry.step`, `set direct.retry.step <0-5000>` | `set direct.retry.step 100` | +| `direct.retry.cr` | Adaptive coding-rate thresholds for direct retry packets. Uses `CR4`, `CR5`, `CR7`, or `CR8`; `CR6` is never selected. | `get direct.retry.cr`, `set direct.retry.cr ,,,`, `set direct.retry.cr off` | `set direct.retry.cr 10.0,7.5,2.5,0` | + +The default adaptive coding-rate profile is `10.0,7.5,2.5,2.5`. +SNR `10.0 dB` and up uses `CR4`, `7.5 dB` and up uses `CR5`, +`2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. If no +recent repeater table entry is available, retry packets use `CR5`. Use +`set direct.retry.cr off` to disable adaptive coding-rate overrides. If +adaptive selection chooses `CR4`, retries after the third attempt use +`CR5`. + +Preset details: + +| Preset | Base | Count | Step | SNR gate | +| --- | ---: | ---: | ---: | --- | +| `infra` | `275 ms` | `4` | `150 ms` | SF floor + `15 dB` | +| `rooftop` | `175 ms` | `15` | `100 ms` | SF floor + `5 dB` | +| `mobile` | `175 ms` | `15` | `50 ms` | SF floor | + +Example for a quiet fixed repeater: + +```text +set retry.preset rooftop +set direct.retry.heard on +set direct.retry.margin 5 +``` + +Example for a moving or weak-link node: + +```text +set retry.preset mobile +set direct.retry.margin 0 +``` + +## Flood And Advert Settings + +Flood retry applies to flood-routed packets. A queued retry is canceled when a qualifying downstream echo is heard. + +| Setting | What it does | How to use | Example | +| --- | --- | --- | --- | +| `flood.retry.count` | Maximum flood retry attempts after initial TX. `0` disables flood retry. | `get flood.retry.count`, `set flood.retry.count <0-15>` | `set flood.retry.count 7` | +| `flood.retry.path` | Maximum path hash count eligible for flood retry, or `off` to disable the gate. | `get flood.retry.path`, `set flood.retry.path <0-63/off>` | `set flood.retry.path 1` | +| `flood.retry.advert` | Allows or blocks retry for node advert packets (`type=4`). Default is `off`. | `get flood.retry.advert`, `set flood.retry.advert on/off` | `set flood.retry.advert off` | +| `flood.retry.prefixes` | Target prefixes. If set, only matching downstream echoes cancel a retry. | `get flood.retry.prefixes`, `set flood.retry.prefixes ` | `set flood.retry.prefixes BEEBB0,425E5C` | +| `flood.retry.ignore` | Ignored prefixes. In non-bridge retry, ignored last-hop echoes do not cancel retry. | `get flood.retry.ignore`, `set flood.retry.ignore ` | `set flood.retry.ignore 71CE82,C7618C` | +| `flood.retry.bridge` | Enables bucket-based bridge retry logic. | `get flood.retry.bridge`, `set flood.retry.bridge on/off` | `set flood.retry.bridge on` | +| `flood.retry.bucket.` | Shows one bridge bucket. Buckets are numbered `1`-`6`. | `get flood.retry.bucket.` | `get flood.retry.bucket.1` | +| `flood.retry.bucket` | Sets bridge bucket prefixes. | `set flood.retry.bucket <1-6> ` | `set flood.retry.bucket 1 71CE82,C7618C` | + +The shared retry preset sets these flood defaults: + +| Preset | Retry count | Path gate | +| --- | ---: | ---: | +| `infra` | `1` | `1` | +| `rooftop` | `3` | `2` | +| `mobile` | `3` | `1` | + +Example for path-gated retry: + +```text +set retry.preset rooftop +set flood.retry.path 1 +set flood.retry.advert off +set flood.retry.ignore 71CE82,C7618C +``` + +## North South Buckets + +Buckets describe groups of repeaters on different sides of this relay. Bucket +numbers do not have built-in meanings; this example uses bucket `1` for North +and bucket `2` for South. + +```text + North bucket 1 + +-----------------------+ + | A1B2C3 D4E5F6 | + | North A North B | + +-----------+-----------+ + | + v + +-----------+ + | This node | + +-----------+ + ^ + | + +-----------+-----------+ + | 71CE82 C7618C | + | South A South B | + +-----------------------+ + South bucket 2 +``` + +Configure the buckets: + +```text +set flood.retry.bridge on +set flood.retry.bucket 1 A1B2C3,D4E5F6 +set flood.retry.bucket 2 71CE82,C7618C +set flood.retry.ignore none +``` + +Packet heard from the North: + +```text + heard source + | + v + +--------------+ retry targets + | North bucket | -----> South bucket + | bucket 1 | -----> Other fresh/unbucketed relays + +--------------+ +``` + +Packet heard from the South: + +```text + heard source + | + v + +--------------+ retry targets + | South bucket | -----> North bucket + | bucket 2 | -----> Other fresh/unbucketed relays + +--------------+ +``` + +Packet heard from an unbucketed or pathless source: + +```text + heard source + | + v + +--------------+ retry targets + | Other bucket | -----> North bucket + | implicit | -----> South bucket + +--------------+ +``` + +Bridge retry stays eligible until every target bucket has been heard or +`flood.retry.count` is exhausted. A configured bucket is a target only when at +least one of its prefixes is fresh in `recent.repeater`. Prefixes in +`flood.retry.ignore` never count as bucket hits. + +## Troubleshooting + +If advert packets are still retrying: + +```text +get flood.retry.advert +set flood.retry.advert off +``` + +If ignored prefixes still appear in `flood retry good` logs: + +```text +get flood.retry.ignore +set flood.retry.ignore +``` + +The ignored prefix must match the last hop shown as `heard=`. For example, this log needs `C7618C` in the ignore list: + +```text +flood retry good (... path=7773D0>C7618C, heard=C7618C ...) +``` + +If retries are too aggressive: + +```text +set flood.retry.count 1 +set flood.retry.path 1 +set direct.retry.count 4 +``` + +If retries are too sparse: + +```text +set flood.retry.count 7 +set flood.retry.path 2 +``` diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index bf2f36c3d9..fdb924ad4c 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -233,6 +233,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 + file.read((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122 file.close(); } @@ -273,6 +274,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 + file.write((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122 file.close(); } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index f4190f30ac..0d687cfd5e 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -166,7 +166,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { public: void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); } - + #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0"); diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 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/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 24c4b1f2a7..333c77bfa8 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -85,6 +85,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t last_millis; uint64_t uptime_millis; unsigned long next_local_advert, next_flood_advert; + unsigned long next_battery_alert_check; + unsigned long last_battery_alert_sent; + bool battery_alert_sent; bool _logging; NodePrefs _prefs; ClientACL acl; @@ -99,6 +102,15 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { RegionEntry* recv_pkt_region; TransportKey default_scope; RateLimiter discover_limiter, anon_limiter; + struct FloodRetryBridgeState { + uint8_t key[MAX_HASH_SIZE]; + uint8_t source_bucket; + uint8_t target_mask; + uint8_t heard_mask; + uint8_t progress_marker; + bool active; + }; + mutable FloodRetryBridgeState flood_retry_bridge_states[MAX_FLOOD_RETRY_SLOTS]; uint32_t pending_discover_tag; unsigned long pending_discover_until; bool region_load_active; @@ -110,8 +122,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 +134,31 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { ESPNowBridge bridge; #endif + bool extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const; + int8_t getDirectRetryMinSNRX4() const; + uint8_t getDirectRetryCodingRateForSNR(int8_t snr_x4) const; + uint8_t getDirectRetryPreset() const; + uint8_t getDirectRetryConfiguredMaxAttempts() const; + uint32_t getDirectRetryAttemptStepMillis() const; + bool hasFloodRetryPrefixes() const; + bool floodRetryPrefixMatches(const mesh::Packet* packet) const; + bool floodRetryLastHopMatches(const mesh::Packet* packet) const; + bool floodRetryPrefixIgnored(const uint8_t* prefix, uint8_t prefix_len) const; + uint8_t floodRetryEffectivePathLength(const mesh::Packet* packet, uint8_t max_hops = 0xFF) const; + bool floodRetryPrefixFresh(const uint8_t* prefix, uint8_t prefix_len) const; + int floodRetryBucketForPrefix(const uint8_t* prefix, uint8_t prefix_len, bool require_fresh, + bool include_other) const; + int floodRetryBucketForPathHop(const uint8_t* prefix, uint8_t prefix_len, uint8_t hop, + uint8_t progress_marker) const; + int floodRetrySourceBucket(const mesh::Packet* packet) const; + uint8_t floodRetryBridgeTargetMask(uint8_t source_bucket) const; + uint8_t floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t source_bucket, + uint8_t progress_marker) const; + FloodRetryBridgeState* floodRetryBridgeStateFor(const mesh::Packet* packet, bool create) const; + void clearFloodRetryBridgeState(const mesh::Packet* packet); + void refreshFloodRetryHeardRecent(const mesh::Packet* packet); + void formatFloodRetryPath(char* dest, size_t dest_len, const mesh::Packet* packet) const; + bool formatFloodRetryHeard(char* dest, size_t dest_len, const mesh::Packet* packet) const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); @@ -126,6 +166,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); + bool sendRepeatersFloodText(const char* text); + void checkBatteryAlert(); File openAppend(const char* fname); bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]); @@ -146,6 +188,20 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t getRetransmitDelay(const mesh::Packet* packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; + uint8_t getDefaultTxCodingRate() const override { return active_cr; } + bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override; + bool maybeShortCircuitDirect(mesh::Packet* packet) override; + void configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) override; + uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override; + uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override; + uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override; + void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override; + bool allowFloodRetry(const mesh::Packet* packet) const override; + void onFloodRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override; + bool hasFloodRetryTargetPrefix(const mesh::Packet* packet) const override; + uint8_t getFloodRetryMaxPathLength(const mesh::Packet* packet) const override; + uint8_t getFloodRetryMaxAttempts(const mesh::Packet* packet) const override; + bool isFloodRetryEchoTarget(const mesh::Packet* packet, uint8_t progress_marker) const override; int getInterferenceThreshold() const override { return _prefs.interference_threshold; @@ -177,6 +233,18 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void onControlDataRecv(mesh::Packet* packet) override; void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size); + mesh::Packet* createPacketCopy(const mesh::Packet* packet, const char* caller); + mesh::Packet* createAltPathCopy(const mesh::Packet* packet, + const uint8_t* primary_path, uint8_t primary_path_len, + const uint8_t* alt_path, uint8_t alt_path_len); + void sendFloodReplyWithAltPath(mesh::Packet* packet, + const uint8_t* direct_path, uint8_t direct_path_len, + const uint8_t* alt_path, uint8_t alt_path_len, + unsigned long delay_millis, uint8_t path_hash_size); + void sendDirectWithAltPath(mesh::Packet* packet, + const uint8_t* path, uint8_t path_len, + const uint8_t* alt_path, uint8_t alt_path_len, + uint32_t delay_millis); public: MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); @@ -226,7 +294,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void saveIdentity(const mesh::LocalIdentity& new_id) override; void clearStats() override; - void handleCommand(uint32_t sender_timestamp, char* command, char* reply); + void handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char* command, char* reply); + void handleCommand(uint32_t sender_timestamp, char* command, char* reply) { + handleCommand(sender_timestamp, NULL, command, reply); + } void loop(); #if defined(WITH_BRIDGE) diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index e9e53ec919..380e54da94 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -225,4 +225,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void clearStats() override; void handleCommand(uint32_t sender_timestamp, char* command, char* reply); void loop(); + + // To check if there is pending work + bool hasPendingWork() const; }; diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index a3798b2175..cbc5243677 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -18,6 +18,9 @@ void halt() { static char command[MAX_POST_TEXT_LEN+1]; +// For power saving +unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot + void setup() { Serial.begin(115200); delay(1000); @@ -115,4 +118,14 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + + if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { +#if defined(NRF52_PLATFORM) + board.sleep(1800); // nrf ignores seconds param, sleeps whenever possible +#else + if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep + board.sleep(1800); // Sleep. Wake up after 30 minutes or when receiving a LoRa packet + } +#endif + } } diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index c0610b7f8a..b870be9e16 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -52,6 +52,13 @@ void Dispatcher::updateTxBudget() { } } +void Dispatcher::restoreOutboundCodingRate() { + if (outbound_restore_cr != 0) { + _radio->setCodingRate(outbound_restore_cr); + outbound_restore_cr = 0; + } +} + int Dispatcher::calcRxDelay(float score, uint32_t air_time) const { return (int) ((pow(10, 0.85f - score) - 1.0) * air_time); } @@ -106,7 +113,9 @@ void Dispatcher::loop() { } _radio->onSendFinished(); + restoreOutboundCodingRate(); logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); + onSendComplete(outbound); if (outbound->isRouteFlood()) { n_sent_flood++; } else { @@ -118,7 +127,9 @@ void Dispatcher::loop() { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime()); _radio->onSendFinished(); + restoreOutboundCodingRate(); logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); + onSendFail(outbound); releasePacket(outbound); // return to pool outbound = NULL; @@ -149,6 +160,7 @@ void Dispatcher::loop() { bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) { int i = 0; + pkt->tx_cr = 0; pkt->header = raw[i++]; if (pkt->getPayloadVer() > PAYLOAD_VER_1) { MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): unsupported packet version", getLogDateTime()); @@ -268,7 +280,10 @@ void Dispatcher::processRecvPacket(Packet* pkt) { uint8_t priority = (action >> 24) - 1; uint32_t _delay = action & 0xFFFFFF; - _mgr->queueOutbound(pkt, priority, futureMillis(_delay)); + if (!queueOutboundPacket(pkt, priority, _delay)) { + onSendFail(pkt); + releasePacket(pkt); + } } } @@ -319,18 +334,32 @@ void Dispatcher::checkSend() { if (len + outbound->payload_len > MAX_TRANS_UNIT) { MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): FATAL: Invalid packet queued... too long, len=%d", getLogDateTime(), len + outbound->payload_len); - _mgr->free(outbound); + onSendFail(outbound); + releasePacket(outbound); outbound = NULL; } else { memcpy(&raw[len], outbound->payload, outbound->payload_len); len += outbound->payload_len; uint32_t max_airtime = _radio->getEstAirtimeFor(len)*3/2; + outbound_restore_cr = 0; + uint8_t default_cr = getDefaultTxCodingRate(); + if (outbound->tx_cr >= 4 && outbound->tx_cr <= 8 && default_cr >= 4 && default_cr <= 8 + && outbound->tx_cr != default_cr) { + if (_radio->setCodingRate(outbound->tx_cr)) { + outbound_restore_cr = default_cr; + max_airtime = _radio->getEstAirtimeFor(len)*3/2; + } else { + MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): WARN: failed to set packet CR%d", getLogDateTime(), (uint32_t)outbound->tx_cr); + } + } outbound_start = _ms->getMillis(); bool success = _radio->startSendRaw(raw, len); if (!success) { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): ERROR: send start failed!", getLogDateTime()); + restoreOutboundCodingRate(); logTxFail(outbound, outbound->getRawLength()); + onSendFail(outbound); releasePacket(outbound); // return to pool outbound = NULL; @@ -360,6 +389,7 @@ Packet* Dispatcher::obtainNewPacket() { } else { pkt->payload_len = pkt->path_len = 0; pkt->_snr = 0; + pkt->tx_cr = 0; } return pkt; } @@ -368,13 +398,21 @@ void Dispatcher::releasePacket(Packet* packet) { _mgr->free(packet); } -void Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) { +bool Dispatcher::queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) { if (!Packet::isValidPathLen(packet->path_len) || packet->payload_len > MAX_PACKET_PAYLOAD) { MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): ERROR: invalid packet... path_len=%d, payload_len=%d", getLogDateTime(), (uint32_t) packet->path_len, (uint32_t) packet->payload_len); - _mgr->free(packet); - } else { - _mgr->queueOutbound(packet, priority, futureMillis(delay_millis)); + return false; } + return _mgr->queueOutbound(packet, priority, futureMillis(delay_millis)); +} + +bool Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) { + if (!queueOutboundPacket(packet, priority, delay_millis)) { + onSendFail(packet); + releasePacket(packet); + return false; + } + return true; } // Utility function -- handles the case where millis() wraps around back to zero @@ -387,4 +425,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 aad6cba3ec..65e3da9e40 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -46,6 +46,12 @@ class Radio { */ virtual bool startSendRaw(const uint8_t* bytes, int len) = 0; + /** + * \brief Sets LoRa coding rate for subsequent transmits/receives. + * \returns true if the radio accepted the coding rate. + */ + virtual bool setCodingRate(uint8_t cr) { return false; } + /** * \returns true if the previous 'startSendRaw()' completed successfully. */ @@ -89,7 +95,7 @@ class PacketManager { virtual Packet* allocNew() = 0; virtual void free(Packet* packet) = 0; - virtual void queueOutbound(Packet* packet, uint8_t priority, uint32_t scheduled_for) = 0; + virtual bool queueOutbound(Packet* packet, uint8_t priority, uint32_t scheduled_for) = 0; virtual Packet* getNextOutbound(uint32_t now) = 0; // by priority virtual int getOutboundCount(uint32_t now) const = 0; virtual int getOutboundTotal() const = 0; @@ -118,6 +124,7 @@ typedef uint32_t DispatcherAction; class Dispatcher { Packet* outbound; // current outbound packet unsigned long outbound_expiry, outbound_start, total_air_time, rx_air_time; + uint8_t outbound_restore_cr; unsigned long next_tx_time; unsigned long cad_busy_start; unsigned long radio_nonrx_start; @@ -130,6 +137,7 @@ class Dispatcher { unsigned long duty_cycle_window_ms; void processRecvPacket(Packet* pkt); + void restoreOutboundCodingRate(); void updateTxBudget(); protected: @@ -142,6 +150,7 @@ class Dispatcher { : _radio(&radio), _ms(&ms), _mgr(&mgr) { outbound = NULL; + outbound_restore_cr = 0; total_air_time = rx_air_time = 0; next_tx_time = ms.getMillis(); cad_busy_start = 0; @@ -161,16 +170,21 @@ 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; virtual int calcRxDelay(float score, uint32_t air_time) const; virtual uint32_t getCADFailRetryDelay() const; virtual uint32_t getCADFailMaxDuration() const; + virtual uint8_t getDefaultTxCodingRate() const { return 0; } virtual int getInterferenceThreshold() const { return 0; } // disabled by default virtual bool getCADEnabled() const { return false; } // hardware CAD disabled by default virtual int getAGCResetInterval() const { return 0; } // disabled by default virtual unsigned long getDutyCycleWindowMs() const { return 3600000; } + const Packet* getOutboundInFlight() const { return outbound; } + bool queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis); public: void begin(); @@ -178,7 +192,7 @@ class Dispatcher { Packet* obtainNewPacket(); void releasePacket(Packet* packet); - void sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0); + bool sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0); unsigned long getTotalAirTime() const { return total_air_time; } unsigned long getReceiveAirTime() const {return rx_air_time; } diff --git a/src/Mesh.cpp b/src/Mesh.cpp index e9b92262ce..71ff104d97 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -3,12 +3,127 @@ namespace mesh { +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 15; +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; +static const uint8_t FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT = 3; +static const uint8_t FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; + +static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) { + uint8_t code = flags & 0x03; + uint8_t size_pow2 = (uint8_t)(1U << code); // legacy TRACE interpretation + uint8_t size_linear = (uint8_t)(code + 1U); // packed-size interpretation (1..4) + + bool pow2_ok = size_pow2 > 0 && (route_bytes % size_pow2) == 0; + bool linear_ok = size_linear > 0 && (route_bytes % size_linear) == 0; + + if (pow2_ok && !linear_ok) { + return size_pow2; + } + if (linear_ok && !pow2_ok) { + return size_linear; + } + if (pow2_ok) { + return size_pow2; + } + return size_linear; +} + void Mesh::begin() { + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + _direct_retries[i].packet = NULL; + _direct_retries[i].trigger_packet = NULL; + _direct_retries[i].retry_started_at = 0; + _direct_retries[i].echo_wait_started_at = 0; + _direct_retries[i].retry_at = 0; + _direct_retries[i].retry_delay = 0; + _direct_retries[i].retry_attempts_sent = 0; + _direct_retries[i].priority = 0; + _direct_retries[i].progress_marker = 0; + _direct_retries[i].expect_path_growth = false; + _direct_retries[i].waiting_final_echo = false; + _direct_retries[i].queued = false; + _direct_retries[i].active = false; + } + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + _flood_retries[i].packet = NULL; + _flood_retries[i].trigger_packet = NULL; + _flood_retries[i].retry_started_at = 0; + _flood_retries[i].retry_at = 0; + _flood_retries[i].retry_delay = 0; + _flood_retries[i].retry_attempts_sent = 0; + _flood_retries[i].priority = 0; + _flood_retries[i].progress_marker = 0; + _flood_retries[i].waiting_final_echo = false; + _flood_retries[i].queued = false; + _flood_retries[i].active = false; + } Dispatcher::begin(); } void Mesh::loop() { Dispatcher::loop(); + + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active) { + continue; + } + + if (_direct_retries[i].waiting_final_echo) { + if (!millisHasNowPassed(_direct_retries[i].retry_at)) { + continue; + } + + uint32_t elapsed_millis = _direct_retries[i].retry_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at); + onDirectRetryEvent("failed_all_tries", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + onDirectRetryEvent("failure", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + clearDirectRetrySlot(i); + continue; + } + + if (!_direct_retries[i].queued || !millisHasNowPassed(_direct_retries[i].retry_at)) { + continue; + } + + if (!isDirectRetryQueued(_direct_retries[i].packet)) { + if (_direct_retries[i].packet == getOutboundInFlight()) { + continue; // currently transmitting; keep slot until onSendComplete/onSendFail emits event + } + clearDirectRetrySlot(i); + } + } + + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + if (!_flood_retries[i].active) { + continue; + } + + if (_flood_retries[i].waiting_final_echo) { + if (!millisHasNowPassed(_flood_retries[i].retry_at)) { + continue; + } + + uint32_t elapsed_millis = _flood_retries[i].retry_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at); + onFloodRetryEvent("failed_all_tries", _flood_retries[i].packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); + onFloodRetryEvent("failure", _flood_retries[i].packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); + clearFloodRetrySlot(i); + continue; + } + + if (!_flood_retries[i].queued || !millisHasNowPassed(_flood_retries[i].retry_at)) { + continue; + } + + if (!isFloodRetryQueued(_flood_retries[i].packet)) { + if (_flood_retries[i].packet == getOutboundInFlight()) { + continue; + } + clearFloodRetrySlot(i); + } + } } bool Mesh::allowPacketForward(const mesh::Packet* packet) { @@ -22,10 +137,56 @@ uint32_t Mesh::getRetransmitDelay(const mesh::Packet* packet) { uint32_t Mesh::getDirectRetransmitDelay(const Packet* packet) { return 0; // by default, no delay } +bool Mesh::allowDirectRetry(const Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { + return false; +} +uint32_t Mesh::getDirectRetryEchoDelay(const Packet* packet) const { + // Keep the base fallback aligned with the repeater's minimum retry wait. + return 200; +} +uint8_t Mesh::getDirectRetryMaxAttempts(const Packet* packet) const { + return DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT; +} +uint32_t Mesh::getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) { + uint32_t base = getDirectRetryEchoDelay(packet); + // Keep the historical linear spacing while allowing the base wait to vary by platform/profile. + return base + ((uint32_t)attempt_idx * 100UL); +} +bool Mesh::allowFloodRetry(const Packet* packet) const { + return true; +} +bool Mesh::hasFloodRetryTargetPrefix(const Packet* packet) const { + return false; +} +uint8_t Mesh::getFloodRetryMaxPathLength(const Packet* packet) const { + return 2; +} +uint8_t Mesh::getFloodRetryMaxAttempts(const Packet* packet) const { + return FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT; +} +uint32_t Mesh::getFloodRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) { + if (packet == NULL) { + return _radio->getEstAirtimeFor(MAX_TRANS_UNIT); + } + + uint32_t max_packet_airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT); + uint32_t packet_airtime = _radio->getEstAirtimeFor(packet->getRawLength()); + return max_packet_airtime + (20UL * packet_airtime); +} uint8_t Mesh::getExtraAckTransmitCount() const { return 0; } +void Mesh::onSendComplete(Packet* packet) { + armDirectRetryOnSendComplete(packet); + armFloodRetryOnSendComplete(packet); +} + +void Mesh::onSendFail(Packet* packet) { + clearPendingDirectRetryOnSendFail(packet); + clearPendingFloodRetryOnSendFail(packet); +} + uint32_t Mesh::getCADFailRetryDelay() const { return _rng->nextInt(1, 4)*120; } @@ -39,6 +200,12 @@ int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int } DispatcherAction Mesh::onRecvPacket(Packet* pkt) { + if (pkt->isRouteDirect()) { + cancelDirectRetryOnEcho(pkt); + } else if (pkt->isRouteFlood()) { + cancelFloodRetryOnEcho(pkt); + } + if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { if (pkt->path_len < MAX_PATH_SIZE) { uint8_t i = 0; @@ -47,19 +214,21 @@ 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 hash_size = decodeTraceHashSize(flags, len); // path_len*entry_size can exceed 255 (path_len up to 63, entry_size up to 8); // a uint8_t offset would wrap and steer the isHashMatch() read to the wrong place. - uint16_t offset = (uint16_t)pkt->path_len << path_sz; + 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? } } @@ -85,7 +254,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } } - if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) && allowPacketForward(pkt)) { + if ((self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) || maybeShortCircuitDirect(pkt)) && allowPacketForward(pkt)) { if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { return forwardMultipartDirect(pkt); } else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { @@ -100,6 +269,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 } } @@ -340,8 +510,10 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) { packet->setPathHashCount(n + 1); uint32_t d = getRetransmitDelay(packet); + uint8_t priority = packet->getPathHashCount(); + maybeScheduleFloodRetry(packet, priority); // as this propagates outwards, give it lower and lower priority - return ACTION_RETRANSMIT_DELAYED(packet->getPathHashCount(), d); // give priority to closer sources, than ones further away + return ACTION_RETRANSMIT_DELAYED(priority, d); // give priority to closer sources, than ones further away } return ACTION_RELEASE; } @@ -375,6 +547,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--; @@ -385,11 +558,565 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len); a2->header &= ~PH_ROUTE_MASK; a2->header |= ROUTE_TYPE_DIRECT; + maybeScheduleDirectRetry(a2, 0); sendPacket(a2, 0, delay_millis); } } } +void Mesh::clearDirectRetrySlot(int idx) { + if (_direct_retries[idx].waiting_final_echo && _direct_retries[idx].packet != NULL) { + releasePacket(_direct_retries[idx].packet); + } + _direct_retries[idx].packet = NULL; + _direct_retries[idx].trigger_packet = NULL; + _direct_retries[idx].retry_started_at = 0; + _direct_retries[idx].echo_wait_started_at = 0; + _direct_retries[idx].retry_at = 0; + _direct_retries[idx].retry_delay = 0; + _direct_retries[idx].retry_attempts_sent = 0; + _direct_retries[idx].priority = 0; + _direct_retries[idx].progress_marker = 0; + _direct_retries[idx].expect_path_growth = false; + _direct_retries[idx].waiting_final_echo = false; + _direct_retries[idx].queued = false; + _direct_retries[idx].active = false; +} + +bool Mesh::isDirectRetryQueued(const Packet* packet) const { + for (int i = 0; i < _mgr->getOutboundTotal(); i++) { + if (_mgr->getOutboundByIdx(i) == packet) { + return true; + } + } + return false; +} + +void Mesh::calculateDirectRetryKey(const Packet* packet, uint8_t* dest_key) const { + uint8_t type = packet->getPayloadType(); + Utils::sha256(dest_key, MAX_HASH_SIZE, &type, 1, packet->payload, packet->payload_len); +} + +bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { + uint8_t recv_key[MAX_HASH_SIZE]; + calculateDirectRetryKey(packet, recv_key); + + bool cleared = false; + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active || memcmp(recv_key, _direct_retries[i].retry_key, MAX_HASH_SIZE) != 0) { + continue; + } + + bool is_echo = _direct_retries[i].expect_path_growth + ? packet->path_len > _direct_retries[i].progress_marker + : packet->getPathHashCount() < _direct_retries[i].progress_marker; + if (!is_echo) { + continue; + } + + int8_t echo_snr_x4 = packet->_snr; + if (_direct_retries[i].queued || _direct_retries[i].waiting_final_echo) { + if (_direct_retries[i].packet != NULL) { + // Success quality comes from the received downstream echo, not the original upstream RX. + _direct_retries[i].packet->_snr = echo_snr_x4; + } + uint32_t echo_millis = _direct_retries[i].echo_wait_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _direct_retries[i].echo_wait_started_at); + uint8_t retry_attempt = _direct_retries[i].waiting_final_echo + ? _direct_retries[i].retry_attempts_sent + : _direct_retries[i].retry_attempts_sent + 1; + onDirectRetryEvent("good", _direct_retries[i].packet, echo_millis, retry_attempt); + if (_direct_retries[i].queued) { + for (int j = 0; j < _mgr->getOutboundTotal(); j++) { + if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { + Packet* pending = _mgr->removeOutboundByIdx(j); + if (pending) { + releasePacket(pending); + } + break; + } + } + } + clearDirectRetrySlot(i); + } else { + if (_direct_retries[i].trigger_packet != NULL) { + _direct_retries[i].trigger_packet->_snr = echo_snr_x4; + } + uint32_t echo_millis = _direct_retries[i].echo_wait_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _direct_retries[i].echo_wait_started_at); + onDirectRetryEvent("good", _direct_retries[i].trigger_packet, echo_millis, _direct_retries[i].retry_attempts_sent + 1); + clearDirectRetrySlot(i); + } + cleared = true; + } + + return cleared; +} + +void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active) { + continue; + } + + if (_direct_retries[i].queued) { + if (_direct_retries[i].packet == packet) { + // The retry packet itself just finished transmitting; Dispatcher will release it after this hook. + uint32_t elapsed_millis = _direct_retries[i].retry_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at); + onDirectRetryEvent("resent", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1); + _direct_retries[i].echo_wait_started_at = _ms->getMillis(); + _direct_retries[i].retry_attempts_sent++; + uint8_t max_attempts = getDirectRetryMaxAttempts(packet); + if (max_attempts < 1) { + max_attempts = 1; + } else if (max_attempts > DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX) { + max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX; + } + if (_direct_retries[i].retry_attempts_sent >= max_attempts) { + Packet* final_wait = obtainNewPacket(); + if (final_wait == NULL) { + onDirectRetryEvent("dropped_no_packet", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + clearDirectRetrySlot(i); + continue; + } + + *final_wait = *packet; + _direct_retries[i].packet = final_wait; + _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); + _direct_retries[i].waiting_final_echo = true; + _direct_retries[i].queued = false; + continue; + } + + Packet* retry = obtainNewPacket(); + if (retry == NULL) { + onDirectRetryEvent("dropped_no_packet", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1); + onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1); + clearDirectRetrySlot(i); + continue; + } + + *retry = *packet; + retry->tx_cr = 0; + uint8_t retry_attempt = _direct_retries[i].retry_attempts_sent + 1; + configureDirectRetryPacket(retry, packet, retry_attempt); + uint32_t retry_delay = getDirectRetryAttemptDelay(packet, _direct_retries[i].retry_attempts_sent); + if (queueOutboundPacket(retry, _direct_retries[i].priority, retry_delay)) { + _direct_retries[i].packet = retry; + _direct_retries[i].retry_delay = retry_delay; + _direct_retries[i].retry_at = futureMillis(retry_delay); + _direct_retries[i].waiting_final_echo = false; + onDirectRetryEvent("queued", retry, retry_delay, retry_attempt); + } else { + onDirectRetryEvent("dropped_queue_full", retry, retry_delay, retry_attempt); + onDirectRetryEvent("failure", retry, elapsed_millis, retry_attempt); + releasePacket(retry); + clearDirectRetrySlot(i); + } + } + continue; + } + + if (_direct_retries[i].trigger_packet != packet) { + continue; + } + + // Allocate the retry packet only after TX-complete so busy repeaters do not reserve pool slots early. + Packet* retry = obtainNewPacket(); + if (retry == NULL) { + onDirectRetryEvent("dropped_no_packet", packet, _direct_retries[i].retry_delay, 1); + onDirectRetryEvent("failure", packet, 0, 1); + clearDirectRetrySlot(i); + continue; + } + + *retry = *packet; + retry->tx_cr = 0; + configureDirectRetryPacket(retry, packet, 1); + + // Start the echo wait only after the initial direct transmission actually completed. + if (queueOutboundPacket(retry, _direct_retries[i].priority, _direct_retries[i].retry_delay)) { + unsigned long now = _ms->getMillis(); + _direct_retries[i].packet = retry; + _direct_retries[i].trigger_packet = NULL; + _direct_retries[i].queued = true; + _direct_retries[i].waiting_final_echo = false; + _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); + _direct_retries[i].retry_started_at = now; + _direct_retries[i].echo_wait_started_at = now; + onDirectRetryEvent("queued", retry, _direct_retries[i].retry_delay, 1); + } else { + onDirectRetryEvent("dropped_queue_full", retry, _direct_retries[i].retry_delay, 1); + onDirectRetryEvent("failure", retry, 0, 1); + releasePacket(retry); + clearDirectRetrySlot(i); + } + } +} + +void Mesh::clearPendingDirectRetryOnSendFail(const Packet* packet) { + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active) { + continue; + } + + if (_direct_retries[i].queued) { + if (_direct_retries[i].packet == packet) { + // The queued retry itself failed; Dispatcher will release it after this hook. + onDirectRetryEvent("dropped_send_fail", packet, 0, _direct_retries[i].retry_attempts_sent + 1); + onDirectRetryEvent("failure", packet, 0, _direct_retries[i].retry_attempts_sent + 1); + clearDirectRetrySlot(i); + } + continue; + } + + if (_direct_retries[i].trigger_packet == packet) { + onDirectRetryEvent("dropped_send_fail", packet, 0, 1); + onDirectRetryEvent("failure", packet, 0, 1); + clearDirectRetrySlot(i); + } + } +} + +bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len, + uint8_t& progress_marker, bool& expect_path_growth) const { + switch (packet->getPayloadType()) { + case PAYLOAD_TYPE_ACK: + case PAYLOAD_TYPE_PATH: + case PAYLOAD_TYPE_REQ: + case PAYLOAD_TYPE_RESPONSE: + case PAYLOAD_TYPE_TXT_MSG: + case PAYLOAD_TYPE_ANON_REQ: + // Allow retries even when only one downstream hop remains so fixed direct paths + // (e.g. remote admin/login over 2-hop chains) use the same retry policy. + if (packet->getPathHashCount() == 0) { + return false; + } + next_hop_hash = packet->path; + next_hop_hash_len = packet->getPathHashSize(); + progress_marker = packet->getPathHashCount(); + expect_path_growth = false; + return true; + + case PAYLOAD_TYPE_MULTIPART: + if (packet->payload_len < 1 || (packet->payload[0] & 0x0F) != PAYLOAD_TYPE_ACK || packet->getPathHashCount() == 0) { + return false; + } + next_hop_hash = packet->path; + next_hop_hash_len = packet->getPathHashSize(); + progress_marker = packet->getPathHashCount(); + expect_path_growth = false; + return true; + + case PAYLOAD_TYPE_TRACE: { + if (packet->payload_len < 9) { + return false; + } + + uint8_t route_bytes = packet->payload_len - 9; + uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes); + uint16_t offset = (uint16_t)packet->path_len * (uint16_t)hash_size; + if (offset + hash_size > route_bytes) { + return false; + } + if (offset + (2 * hash_size) > route_bytes) { + return false; // no downstream repeater means there will be no forward echo to overhear. + } + + next_hop_hash = &packet->payload[9 + offset]; + next_hop_hash_len = hash_size; + progress_marker = packet->path_len; + expect_path_growth = true; + return true; + } + + default: + return false; + } +} + +void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { + const uint8_t* next_hop_hash; + uint8_t next_hop_hash_len; + uint8_t progress_marker; + bool expect_path_growth; + if (!getDirectRetryTarget(packet, next_hop_hash, next_hop_hash_len, progress_marker, expect_path_growth) + || !allowDirectRetry(packet, next_hop_hash, next_hop_hash_len)) { + return; + } + + int slot_idx = -1; + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active) { + slot_idx = i; + break; + } + } + if (slot_idx < 0) { + onDirectRetryEvent("dropped_no_slot", packet, 0, 0); + onDirectRetryEvent("failure", packet, 0, 0); + return; + } + + // Only store retry metadata here; allocate the retry packet after the initial TX really completes. + uint32_t retry_delay = getDirectRetryAttemptDelay(packet, 0); + calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key); + _direct_retries[slot_idx].packet = NULL; + _direct_retries[slot_idx].trigger_packet = const_cast(packet); + _direct_retries[slot_idx].retry_started_at = 0; + _direct_retries[slot_idx].echo_wait_started_at = 0; + _direct_retries[slot_idx].retry_at = 0; + _direct_retries[slot_idx].retry_delay = retry_delay; + _direct_retries[slot_idx].retry_attempts_sent = 0; + _direct_retries[slot_idx].priority = priority; + _direct_retries[slot_idx].progress_marker = progress_marker; + _direct_retries[slot_idx].expect_path_growth = expect_path_growth; + _direct_retries[slot_idx].waiting_final_echo = false; + _direct_retries[slot_idx].queued = false; + _direct_retries[slot_idx].active = true; +} + +void Mesh::clearFloodRetrySlot(int idx) { + if (_flood_retries[idx].waiting_final_echo && _flood_retries[idx].packet != NULL) { + releasePacket(_flood_retries[idx].packet); + } + _flood_retries[idx].packet = NULL; + _flood_retries[idx].trigger_packet = NULL; + _flood_retries[idx].retry_started_at = 0; + _flood_retries[idx].retry_at = 0; + _flood_retries[idx].retry_delay = 0; + _flood_retries[idx].retry_attempts_sent = 0; + _flood_retries[idx].priority = 0; + _flood_retries[idx].progress_marker = 0; + _flood_retries[idx].waiting_final_echo = false; + _flood_retries[idx].queued = false; + _flood_retries[idx].active = false; +} + +bool Mesh::isFloodRetryQueued(const Packet* packet) const { + for (int i = 0; i < _mgr->getOutboundTotal(); i++) { + if (_mgr->getOutboundByIdx(i) == packet) { + return true; + } + } + return false; +} + +bool Mesh::isFloodRetryEchoTarget(const Packet* packet, uint8_t progress_marker) const { + return packet->isRouteFlood() && packet->getPathHashCount() > progress_marker; +} + +bool Mesh::cancelFloodRetryOnEcho(const Packet* packet) { + uint8_t recv_key[MAX_HASH_SIZE]; + packet->calculatePacketHash(recv_key); + + bool cleared = false; + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + if (!_flood_retries[i].active || memcmp(recv_key, _flood_retries[i].retry_key, MAX_HASH_SIZE) != 0) { + continue; + } + if (!isFloodRetryEchoTarget(packet, _flood_retries[i].progress_marker)) { + continue; + } + + uint32_t echo_millis = _flood_retries[i].retry_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at); + uint8_t retry_attempt = _flood_retries[i].waiting_final_echo + ? _flood_retries[i].retry_attempts_sent + : _flood_retries[i].retry_attempts_sent + 1; + onFloodRetryEvent("good", packet, echo_millis, retry_attempt); + + if (_flood_retries[i].queued) { + for (int j = 0; j < _mgr->getOutboundTotal(); j++) { + if (_mgr->getOutboundByIdx(j) == _flood_retries[i].packet) { + Packet* pending = _mgr->removeOutboundByIdx(j); + if (pending) { + releasePacket(pending); + } + break; + } + } + } + clearFloodRetrySlot(i); + cleared = true; + } + + return cleared; +} + +void Mesh::armFloodRetryOnSendComplete(const Packet* packet) { + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + if (!_flood_retries[i].active) { + continue; + } + + if (_flood_retries[i].queued) { + if (_flood_retries[i].packet != packet) { + continue; + } + + uint32_t elapsed_millis = _flood_retries[i].retry_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at); + onFloodRetryEvent("resent", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); + _flood_retries[i].retry_attempts_sent++; + + uint8_t max_attempts = getFloodRetryMaxAttempts(packet); + if (max_attempts < 1) { + max_attempts = 1; + } else if (max_attempts > FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX) { + max_attempts = FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX; + } + if (_flood_retries[i].retry_attempts_sent >= max_attempts) { + Packet* final_wait = obtainNewPacket(); + if (final_wait == NULL) { + onFloodRetryEvent("dropped_no_packet", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); + onFloodRetryEvent("failure", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); + clearFloodRetrySlot(i); + continue; + } + + *final_wait = *packet; + _flood_retries[i].packet = final_wait; + _flood_retries[i].retry_at = futureMillis(_flood_retries[i].retry_delay); + _flood_retries[i].waiting_final_echo = true; + _flood_retries[i].queued = false; + continue; + } + + Packet* retry = obtainNewPacket(); + if (retry == NULL) { + onFloodRetryEvent("dropped_no_packet", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); + onFloodRetryEvent("failure", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); + clearFloodRetrySlot(i); + continue; + } + + *retry = *packet; + uint32_t retry_delay = getFloodRetryAttemptDelay(packet, _flood_retries[i].retry_attempts_sent); + if (queueOutboundPacket(retry, _flood_retries[i].priority, retry_delay)) { + _flood_retries[i].packet = retry; + _flood_retries[i].retry_delay = retry_delay; + _flood_retries[i].retry_at = futureMillis(retry_delay); + _flood_retries[i].retry_started_at = _ms->getMillis(); + _flood_retries[i].waiting_final_echo = false; + onFloodRetryEvent("queued", retry, retry_delay, _flood_retries[i].retry_attempts_sent + 1); + } else { + onFloodRetryEvent("dropped_queue_full", retry, retry_delay, _flood_retries[i].retry_attempts_sent + 1); + onFloodRetryEvent("failure", retry, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); + releasePacket(retry); + clearFloodRetrySlot(i); + } + continue; + } + + if (_flood_retries[i].trigger_packet != packet) { + continue; + } + + Packet* retry = obtainNewPacket(); + if (retry == NULL) { + onFloodRetryEvent("dropped_no_packet", packet, _flood_retries[i].retry_delay, 1); + onFloodRetryEvent("failure", packet, 0, 1); + clearFloodRetrySlot(i); + continue; + } + + *retry = *packet; + if (queueOutboundPacket(retry, _flood_retries[i].priority, _flood_retries[i].retry_delay)) { + unsigned long now = _ms->getMillis(); + _flood_retries[i].packet = retry; + _flood_retries[i].trigger_packet = NULL; + _flood_retries[i].queued = true; + _flood_retries[i].waiting_final_echo = false; + _flood_retries[i].retry_at = futureMillis(_flood_retries[i].retry_delay); + _flood_retries[i].retry_started_at = now; + onFloodRetryEvent("queued", retry, _flood_retries[i].retry_delay, 1); + } else { + onFloodRetryEvent("dropped_queue_full", retry, _flood_retries[i].retry_delay, 1); + onFloodRetryEvent("failure", retry, 0, 1); + releasePacket(retry); + clearFloodRetrySlot(i); + } + } +} + +void Mesh::clearPendingFloodRetryOnSendFail(const Packet* packet) { + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + if (!_flood_retries[i].active) { + continue; + } + + if (_flood_retries[i].queued) { + if (_flood_retries[i].packet == packet) { + onFloodRetryEvent("dropped_send_fail", packet, 0, _flood_retries[i].retry_attempts_sent + 1); + onFloodRetryEvent("failure", packet, 0, _flood_retries[i].retry_attempts_sent + 1); + clearFloodRetrySlot(i); + } + continue; + } + + if (_flood_retries[i].trigger_packet == packet) { + onFloodRetryEvent("dropped_send_fail", packet, 0, 1); + onFloodRetryEvent("failure", packet, 0, 1); + clearFloodRetrySlot(i); + } + } +} + +void Mesh::maybeScheduleFloodRetry(const Packet* packet, uint8_t priority) { + if (packet == NULL || !packet->isRouteFlood() || hasFloodRetryTargetPrefix(packet)) { + return; + } + + uint8_t max_path_len = getFloodRetryMaxPathLength(packet); + if (max_path_len != FLOOD_RETRY_PATH_GATE_DISABLED && packet->getPathHashCount() > max_path_len) { + return; + } + + uint8_t max_attempts = getFloodRetryMaxAttempts(packet); + if (max_attempts == 0) { + return; + } + + int slot_idx = -1; + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + if (!_flood_retries[i].active) { + slot_idx = i; + break; + } + } + if (slot_idx < 0) { + onFloodRetryEvent("dropped_no_slot", packet, 0, 0); + onFloodRetryEvent("failure", packet, 0, 0); + return; + } + + if (!allowFloodRetry(packet)) { + return; + } + + uint32_t retry_delay = getFloodRetryAttemptDelay(packet, 0); + packet->calculatePacketHash(_flood_retries[slot_idx].retry_key); + _flood_retries[slot_idx].packet = NULL; + _flood_retries[slot_idx].trigger_packet = const_cast(packet); + _flood_retries[slot_idx].retry_started_at = 0; + _flood_retries[slot_idx].retry_at = 0; + _flood_retries[slot_idx].retry_delay = retry_delay; + _flood_retries[slot_idx].retry_attempts_sent = 0; + _flood_retries[slot_idx].priority = priority; + _flood_retries[slot_idx].progress_marker = packet->getPathHashCount(); + _flood_retries[slot_idx].waiting_final_echo = false; + _flood_retries[slot_idx].queued = false; + _flood_retries[slot_idx].active = true; +} + Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) { if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL; @@ -637,7 +1364,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) { @@ -666,7 +1393,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) { @@ -699,7 +1426,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); } @@ -709,7 +1437,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); } @@ -722,9 +1450,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 2302d6b5b7..3b3f903dca 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -4,6 +4,18 @@ namespace mesh { +#ifndef MAX_DIRECT_RETRY_SLOTS + #define MAX_DIRECT_RETRY_SLOTS 6 +#endif + +#ifndef MAX_FLOOD_RETRY_SLOTS + #define MAX_FLOOD_RETRY_SLOTS 6 +#endif + +#ifndef FLOOD_RETRY_PATH_GATE_DISABLED + #define FLOOD_RETRY_PATH_GATE_DISABLED 0xFF +#endif + class GroupChannel { public: uint8_t hash[PATH_HASH_SIZE]; @@ -16,6 +28,7 @@ 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 +37,68 @@ 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; + }; + + struct FloodRetryEntry { + Packet* packet; + Packet* trigger_packet; + unsigned long retry_started_at; + unsigned long retry_at; + uint32_t retry_delay; + uint8_t retry_attempts_sent; + uint8_t retry_key[MAX_HASH_SIZE]; + uint8_t priority; + uint8_t progress_marker; + bool waiting_final_echo; + bool queued; + bool active; + }; + RTCClock* _rtc; RNG* _rng; MeshTables* _tables; + DirectRetryEntry _direct_retries[MAX_DIRECT_RETRY_SLOTS]; + FloodRetryEntry _flood_retries[MAX_FLOOD_RETRY_SLOTS]; void removeSelfFromPath(Packet* packet); void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis); + void clearDirectRetrySlot(int idx); + bool isDirectRetryQueued(const Packet* packet) const; + void calculateDirectRetryKey(const Packet* packet, uint8_t* dest_key) const; + bool cancelDirectRetryOnEcho(const Packet* packet); + void armDirectRetryOnSendComplete(const Packet* packet); + void clearPendingDirectRetryOnSendFail(const Packet* packet); + bool getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len, + uint8_t& progress_marker, bool& expect_path_growth) const; + void maybeScheduleDirectRetry(const Packet* packet, uint8_t priority); + void clearFloodRetrySlot(int idx); + bool isFloodRetryQueued(const Packet* packet) const; + bool cancelFloodRetryOnEcho(const Packet* packet); + void armFloodRetryOnSendComplete(const Packet* packet); + void clearPendingFloodRetryOnSendFail(const Packet* packet); + void maybeScheduleFloodRetry(const Packet* packet, uint8_t priority); //void routeRecvAcks(Packet* packet, uint32_t delay_millis); DispatcherAction forwardMultipartDirect(Packet* pkt); protected: DispatcherAction onRecvPacket(Packet* pkt) override; + void onSendComplete(Packet* packet) override; + void onSendFail(Packet* packet) override; virtual uint32_t getCADFailRetryDelay() const override; @@ -65,11 +129,82 @@ 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; + + /** + * \brief Allow subclasses to rewrite a non-TRACE DIRECT packet path when this node can safely skip ahead. + */ + virtual bool maybeShortCircuitDirect(Packet* packet) { return false; } + + /** + * \returns milliseconds to wait for the next-hop echo before queueing one retry of the DIRECT packet. + */ + virtual uint32_t getDirectRetryEchoDelay(const Packet* packet) const; + + /** + * \returns maximum number of retry transmissions after the initial direct TX. + */ + virtual uint8_t getDirectRetryMaxAttempts(const Packet* packet) const; + + /** + * \returns delay before a specific retry attempt, where attempt_idx=0 is the first retry. + */ + virtual uint32_t getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx); + + /** + * \brief Decide whether a FLOOD packet should retry when no downstream echo is overheard. + */ + virtual bool allowFloodRetry(const Packet* packet) const; + + /** + * \brief Return true when this FLOOD packet already carries an application-defined target prefix. + */ + virtual bool hasFloodRetryTargetPrefix(const Packet* packet) const; + + /** + * \returns maximum flood path hash count eligible for retry, or FLOOD_RETRY_PATH_GATE_DISABLED. + */ + virtual uint8_t getFloodRetryMaxPathLength(const Packet* packet) const; + + /** + * \returns maximum number of FLOOD retry transmissions after the initial TX. + */ + virtual uint8_t getFloodRetryMaxAttempts(const Packet* packet) const; + + /** + * \brief Return true when a received FLOOD echo is enough to cancel a pending retry. + */ + virtual bool isFloodRetryEchoTarget(const Packet* packet, uint8_t progress_marker) const; + + /** + * \returns delay before a specific flood retry attempt, where attempt_idx=0 is the first retry. + */ + virtual uint32_t getFloodRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx); + + /** + * \brief Optional hook for logging flood-retry lifecycle events. + */ + virtual void onFloodRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { } + /** * \returns number of extra (Direct) ACK transmissions wanted. */ virtual uint8_t getExtraAckTransmitCount() const; + /** + * \brief Optional hook for logging direct-retry lifecycle events. + */ + virtual void onDirectRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { } + + /** + * \brief Optional hook to set local-only transmit options on a retry packet before it is queued. + */ + virtual void configureDirectRetryPacket(Packet* retry, const Packet* original, uint8_t retry_attempt) { } + /** * \brief Perform search of local DB of peers/contacts. * \returns Number of peers with matching hash diff --git a/src/Packet.cpp b/src/Packet.cpp index aad3e2f48e..3aab6349c5 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -8,6 +8,7 @@ Packet::Packet() { header = 0; path_len = 0; payload_len = 0; + tx_cr = 0; } bool Packet::isValidPathLen(uint8_t path_len) { @@ -64,6 +65,7 @@ uint8_t Packet::writeTo(uint8_t dest[]) const { bool Packet::readFrom(const uint8_t src[], uint8_t len) { uint8_t i = 0; + tx_cr = 0; header = src[i++]; if (hasTransportCodes()) { memcpy(&transport_codes[0], &src[i], 2); i += 2; @@ -84,4 +86,4 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) { return true; // success } -} \ No newline at end of file +} diff --git a/src/Packet.h b/src/Packet.h index 0886a06c4e..2943c03770 100644 --- a/src/Packet.h +++ b/src/Packet.h @@ -49,6 +49,7 @@ class Packet { uint8_t path[MAX_PATH_SIZE]; uint8_t payload[MAX_PACKET_PAYLOAD]; int8_t _snr; + uint8_t tx_cr; // volatile local-only TX coding-rate override; not serialized /** * \brief calculate the hash of payload + type diff --git a/src/helpers/ArduinoSerialInterface.cpp b/src/helpers/ArduinoSerialInterface.cpp index a01fa5866f..6b44397406 100644 --- a/src/helpers/ArduinoSerialInterface.cpp +++ b/src/helpers/ArduinoSerialInterface.cpp @@ -17,6 +17,10 @@ bool ArduinoSerialInterface::isConnected() const { return true; // no way of knowing, so assume yes } +bool ArduinoSerialInterface::isReadBusy() const { + return false; +} + bool ArduinoSerialInterface::isWriteBusy() const { return false; } diff --git a/src/helpers/ArduinoSerialInterface.h b/src/helpers/ArduinoSerialInterface.h index c4086353aa..4fa2b75d14 100644 --- a/src/helpers/ArduinoSerialInterface.h +++ b/src/helpers/ArduinoSerialInterface.h @@ -28,7 +28,8 @@ class ArduinoSerialInterface : public BaseSerialInterface { bool isConnected() const override; + bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; -}; \ No newline at end of file +}; diff --git a/src/helpers/BaseSerialInterface.h b/src/helpers/BaseSerialInterface.h index e9a3f2ab46..8ff110eb57 100644 --- a/src/helpers/BaseSerialInterface.h +++ b/src/helpers/BaseSerialInterface.h @@ -15,6 +15,7 @@ class BaseSerialInterface { virtual bool isConnected() const = 0; + virtual bool isReadBusy() const = 0; virtual bool isWriteBusy() const = 0; virtual size_t writeFrame(const uint8_t src[], size_t len) = 0; virtual size_t checkRecvFrame(uint8_t dest[]) = 0; diff --git a/src/helpers/ClientACL.cpp b/src/helpers/ClientACL.cpp index 1282382737..1d88082349 100644 --- a/src/helpers/ClientACL.cpp +++ b/src/helpers/ClientACL.cpp @@ -1,5 +1,7 @@ #include "ClientACL.h" +static const uint8_t CONTACT_RECORD_VERSION_ALT_PATH = 1; + static File openWrite(FILESYSTEM* _fs, const char* filename) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) _fs->remove(filename); @@ -28,6 +30,7 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) { uint8_t unused[2]; memset(&c, 0, sizeof(c)); + c.alt_path_len = OUT_PATH_UNKNOWN; bool success = (file.read(pub_key, 32) == 32); success = success && (file.read((uint8_t *) &c.permissions, 1) == 1); @@ -36,6 +39,10 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) { success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1); success = success && (file.read(c.out_path, 64) == 64); success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); // will be recalculated below + if (success && unused[0] >= CONTACT_RECORD_VERSION_ALT_PATH) { + success = success && (file.read((uint8_t *)&c.alt_path_len, 1) == 1); + success = success && (file.read(c.alt_path, 64) == 64); + } if (!success) break; // EOF @@ -57,7 +64,8 @@ void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) { File file = openWrite(_fs, "/s_contacts"); if (file) { uint8_t unused[2]; - memset(unused, 0, sizeof(unused)); + unused[0] = CONTACT_RECORD_VERSION_ALT_PATH; + unused[1] = 0; for (int i = 0; i < num_clients; i++) { auto c = &clients[i]; @@ -70,6 +78,8 @@ void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) { success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1); success = success && (file.write(c->out_path, 64) == 64); success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); + success = success && (file.write((uint8_t *)&c->alt_path_len, 1) == 1); + success = success && (file.write(c->alt_path, 64) == 64); if (!success) break; // write failed } @@ -115,6 +125,7 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) { c->permissions = init_perms; c->id = id; c->out_path_len = OUT_PATH_UNKNOWN; + c->alt_path_len = OUT_PATH_UNKNOWN; return c; } diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index b758f7068d..356574de53 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -10,13 +10,16 @@ #define PERM_ACL_READ_WRITE 2 #define PERM_ACL_ADMIN 3 -#define OUT_PATH_UNKNOWN 0xFF +#define OUT_PATH_FORCE_FLOOD 0xFE +#define OUT_PATH_UNKNOWN 0xFF struct ClientInfo { mesh::Identity id; uint8_t permissions; uint8_t out_path_len; uint8_t out_path[MAX_PATH_SIZE]; + uint8_t alt_path_len; + uint8_t alt_path[MAX_PATH_SIZE]; uint8_t shared_secret[PUB_KEY_SIZE]; uint32_t last_timestamp; // by THEIR clock (transient) uint32_t last_activity; // by OUR clock (transient) 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/StaticPoolPacketManager.cpp b/src/helpers/StaticPoolPacketManager.cpp index b8926df0cc..fc2bb059fa 100644 --- a/src/helpers/StaticPoolPacketManager.cpp +++ b/src/helpers/StaticPoolPacketManager.cpp @@ -83,11 +83,12 @@ void StaticPoolPacketManager::free(mesh::Packet* packet) { unused.add(packet, 0, 0); } -void StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { +bool StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { if (!send_queue.add(packet, priority, scheduled_for)) { MESH_DEBUG_PRINTLN("queueOutbound: send queue full, dropping packet"); - free(packet); + return false; } + return true; } mesh::Packet* StaticPoolPacketManager::getNextOutbound(uint32_t now) { diff --git a/src/helpers/StaticPoolPacketManager.h b/src/helpers/StaticPoolPacketManager.h index 59715b4e01..350e85d29d 100644 --- a/src/helpers/StaticPoolPacketManager.h +++ b/src/helpers/StaticPoolPacketManager.h @@ -26,7 +26,7 @@ class StaticPoolPacketManager : public mesh::PacketManager { mesh::Packet* allocNew() override; void free(mesh::Packet* packet) override; - void queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) override; + bool queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) override; mesh::Packet* getNextOutbound(uint32_t now) override; int getOutboundCount(uint32_t now) const override; int getOutboundTotal() const override; @@ -35,4 +35,4 @@ class StaticPoolPacketManager : public mesh::PacketManager { mesh::Packet* removeOutboundByIdx(int i) override; void queueInbound(mesh::Packet* packet, uint32_t scheduled_for) override; mesh::Packet* getNextInbound(uint32_t now) override; -}; \ No newline at end of file +}; 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/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.h b/src/helpers/sensors/EnvironmentSensorManager.h index 29147c8967..b91d2dc38e 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -41,6 +41,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 || defined(ENV_INCLUDE_BME680_BSEC) diff --git a/variants/gat562_30s_mesh_kit/platformio.ini b/variants/gat562_30s_mesh_kit/platformio.ini index 2baac2561b..06d1f0e70a 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 b3e894174a..d7de585ad3 100644 --- a/variants/gat562_mesh_evb_pro/platformio.ini +++ b/variants/gat562_mesh_evb_pro/platformio.ini @@ -5,7 +5,7 @@ board_check = true build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_evb_pro - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D RADIO_CLASS=CustomSX1262 diff --git a/variants/gat562_mesh_tracker_pro/platformio.ini b/variants/gat562_mesh_tracker_pro/platformio.ini index af153b8fc2..df07265f3d 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 f3510b74aa..f457424f10 100644 --- a/variants/gat562_mesh_watch13/platformio.ini +++ b/variants/gat562_mesh_watch13/platformio.ini @@ -8,7 +8,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/gat562_mesh_watch13 -D RAK_4631 -D RAK_BOARD - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/heltec_t096/LoRaFEMControl.h b/variants/heltec_t096/LoRaFEMControl.h index 0ce60fffd8..a3b5c4ed9b 100644 --- a/variants/heltec_t096/LoRaFEMControl.h +++ b/variants/heltec_t096/LoRaFEMControl.h @@ -17,6 +17,6 @@ class LoRaFEMControl bool isLNAEnabled(void) const { return lna_enabled; } private: - bool lna_enabled = false; + bool lna_enabled = true; bool lna_can_control = false; }; diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index e820bf58d3..a37ee57551 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/heltec_t096 -I src/helpers/ui -D HELTEC_T096 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=21 -D P_LORA_NSS=5 -D P_LORA_RESET=16 @@ -143,6 +143,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 135babb1a2..e808d6c287 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -12,7 +12,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/heltec_t114 -I src/helpers/ui -D HELTEC_T114 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=20 -D P_LORA_NSS=24 -D P_LORA_RESET=25 diff --git a/variants/heltec_tracker/platformio.ini b/variants/heltec_tracker/platformio.ini index 69293d7070..797f01af3e 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/LoRaFEMControl.h b/variants/heltec_tracker_v2/LoRaFEMControl.h index 0ce60fffd8..a3b5c4ed9b 100644 --- a/variants/heltec_tracker_v2/LoRaFEMControl.h +++ b/variants/heltec_tracker_v2/LoRaFEMControl.h @@ -17,6 +17,6 @@ class LoRaFEMControl bool isLNAEnabled(void) const { return lna_enabled; } private: - bool lna_enabled = false; + bool lna_enabled = true; bool lna_can_control = false; }; diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index d57c2113f0..248fdf5af3 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/platformio.ini b/variants/heltec_v2/platformio.ini index ba4f869422..1cfc7531dd 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -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_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 a70a93a508..0bf5dd9de0 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/platformio.ini b/variants/heltec_v4/platformio.ini index fabf38272d..875f4b8712 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -222,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 48723d169a..7c15581053 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_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index c7a595520e..f79a6f4aa2 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/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 20a8a548b9..03cb226e97 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 2bbba31463..53ded94517 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/rak4631 -D RAK_4631 -D RAK_BOARD - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_GPS_TX=PIN_SERIAL1_RX diff --git a/variants/sensecap_solar/platformio.ini b/variants/sensecap_solar/platformio.ini index effef38ccd..c9f7ed397d 100644 --- a/variants/sensecap_solar/platformio.ini +++ b/variants/sensecap_solar/platformio.ini @@ -10,7 +10,7 @@ build_flags = ${nrf52_base.build_flags} -I src/helpers/nrf52 -D NRF52_PLATFORM=1 -D USE_SX1262 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D P_LORA_TX_LED=12 diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index c0e8458d0e..152c74be06 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_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index db8c5a9486..53a9e2b8b2 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 =