mirror of
https://github.com/agessaman/MeshCore.git
synced 2026-08-28 18:08:16 +00:00
Tune keymind repeater behavior
This commit is contained in:
@@ -7,6 +7,9 @@ MEMORY
|
||||
{
|
||||
FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xD4000 - 0x27000
|
||||
|
||||
/* To keep data in RAM across resets */
|
||||
PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8
|
||||
|
||||
/* SRAM required by Softdevice depend on
|
||||
* - Attribute Table Size (Number of Services and Characteristics)
|
||||
* - Vendor UUID count
|
||||
|
||||
@@ -15,6 +15,7 @@ RADIO_FREQ_OVERRIDE=""
|
||||
RADIO_BW_OVERRIDE=""
|
||||
RADIO_SF_OVERRIDE=""
|
||||
RADIO_CR_OVERRIDE=""
|
||||
FIRMWARE_PROFILE_OVERRIDE="${FIRMWARE_PROFILE_OVERRIDE:-}"
|
||||
BATCH_BUILD_MODE=0
|
||||
RESOLVED_BUILD_TARGETS=()
|
||||
|
||||
@@ -60,7 +61,7 @@ Examples:
|
||||
Build firmware for the "RAK_4631_repeater" device target
|
||||
$ bash build.sh build-firmware RAK_4631_repeater
|
||||
|
||||
Run without arguments to choose an interactive build action/target, debug options, radio settings, and firmware version
|
||||
Run without arguments to choose an interactive build action/target, debug options, radio settings, firmware profile, and firmware version
|
||||
$ bash build.sh
|
||||
|
||||
Build all firmwares for device targets containing the string "RAK_4631"
|
||||
@@ -315,6 +316,10 @@ clear_radio_overrides() {
|
||||
RADIO_CR_OVERRIDE=""
|
||||
}
|
||||
|
||||
clear_firmware_profile_overrides() {
|
||||
FIRMWARE_PROFILE_OVERRIDE=""
|
||||
}
|
||||
|
||||
set_radio_overrides() {
|
||||
RADIO_SETTING_TITLE=$1
|
||||
RADIO_FREQ_OVERRIDE=$2
|
||||
@@ -323,6 +328,10 @@ set_radio_overrides() {
|
||||
RADIO_CR_OVERRIDE=$5
|
||||
}
|
||||
|
||||
set_firmware_profile_override() {
|
||||
FIRMWARE_PROFILE_OVERRIDE=$1
|
||||
}
|
||||
|
||||
fetch_suggested_radio_settings() {
|
||||
python3 - "$RADIO_SETTINGS_API_URL" <<'PY'
|
||||
import json
|
||||
@@ -367,7 +376,7 @@ is_valid_custom_radio_bandwidth() {
|
||||
python3 - "$1" <<'PY'
|
||||
import sys
|
||||
|
||||
allowed = [7.81, 10.42, 15.63, 20.83, 31.25, 41.67, 62.5, 125.0, 250.0, 500.0]
|
||||
allowed = [7.8, 10.4, 15.6, 20.8, 31.25, 41.7, 62.5, 125.0, 250.0, 500.0]
|
||||
try:
|
||||
value = float(sys.argv[1])
|
||||
except Exception:
|
||||
@@ -403,13 +412,13 @@ prompt_for_custom_radio_setting() {
|
||||
echo "Please enter 5, 6, 7, 8, 9, 10, 11, or 12."
|
||||
done
|
||||
|
||||
echo "Bandwidth options (kHz): 7.81 10.42 15.63 20.83 31.25 41.67 62.5 125 250 500"
|
||||
echo "Bandwidth options (kHz): 7.8 10.4 15.6 20.8 31.25 41.7 62.5 125 250 500"
|
||||
while true; do
|
||||
read -r -p "BW (kHz): " bw
|
||||
if [[ "$bw" =~ ^[0-9]+([.][0-9]+)?$ ]] && is_valid_custom_radio_bandwidth "$bw"; then
|
||||
break
|
||||
fi
|
||||
echo "Please enter one of: 7.81 10.42 15.63 20.83 31.25 41.67 62.5 125 250 500."
|
||||
echo "Please enter one of: 7.8 10.4 15.6 20.8 31.25 41.7 62.5 125 250 500."
|
||||
done
|
||||
|
||||
echo "Coding rate options: CR5, CR6, CR7, CR8"
|
||||
@@ -426,6 +435,7 @@ prompt_for_custom_radio_setting() {
|
||||
|
||||
prompt_for_radio_build_settings() {
|
||||
local -a preset_rows=()
|
||||
local -a fetched_preset_rows=()
|
||||
local -a options=("Keep target defaults (no radio override)")
|
||||
local row
|
||||
local title
|
||||
@@ -443,20 +453,26 @@ prompt_for_radio_build_settings() {
|
||||
|
||||
if preset_output=$(fetch_suggested_radio_settings); then
|
||||
if [ -n "$preset_output" ]; then
|
||||
mapfile -t preset_rows <<< "$preset_output"
|
||||
mapfile -t fetched_preset_rows <<< "$preset_output"
|
||||
fi
|
||||
for row in "${preset_rows[@]}"; do
|
||||
for row in "${fetched_preset_rows[@]}"; do
|
||||
if [ -z "$row" ]; then
|
||||
continue
|
||||
fi
|
||||
IFS=$'\t' read -r title description freq bw sf cr <<< "$row"
|
||||
options+=("${title}: ${description}")
|
||||
preset_rows+=("$row")
|
||||
done
|
||||
else
|
||||
echo "Could not fetch radio presets from ${RADIO_SETTINGS_API_URL}."
|
||||
preset_rows=()
|
||||
fi
|
||||
|
||||
for row in "${preset_rows[@]}"; do
|
||||
if [ -z "$row" ]; then
|
||||
continue
|
||||
fi
|
||||
IFS=$'\t' read -r title description freq bw sf cr <<< "$row"
|
||||
options+=("${title}: ${description}")
|
||||
done
|
||||
|
||||
options+=("Custom")
|
||||
custom_index=${#options[@]}
|
||||
|
||||
@@ -491,6 +507,37 @@ prompt_for_radio_build_settings() {
|
||||
done
|
||||
}
|
||||
|
||||
prompt_for_firmware_profile_settings() {
|
||||
local -a options=(
|
||||
"Keep target defaults"
|
||||
"Cascade: path.hash.mode=2 / loop.detect=minimal / rxdelay=2 / agc.reset.interval=8 / advert.interval=0 / flood.advert.interval=83 / multi.acks=1"
|
||||
)
|
||||
|
||||
clear_firmware_profile_overrides
|
||||
|
||||
echo "Set firmware profile options:"
|
||||
while true; do
|
||||
print_numbered_menu "${options[@]}"
|
||||
prompt_menu_choice "Firmware profile" "${#options[@]}"
|
||||
if [ "$MENU_CHOICE" == "QUIT" ]; then
|
||||
echo "Cancelled."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$MENU_CHOICE" in
|
||||
1)
|
||||
echo "Using target default firmware profile settings."
|
||||
return 0
|
||||
;;
|
||||
2)
|
||||
set_firmware_profile_override "cascade"
|
||||
echo "Using firmware profile: Cascade"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
get_env_metadata() {
|
||||
local env_name=$1
|
||||
local trimmed_env_name
|
||||
@@ -966,6 +1013,14 @@ apply_radio_overrides() {
|
||||
fi
|
||||
}
|
||||
|
||||
apply_firmware_profile_overrides() {
|
||||
case "${FIRMWARE_PROFILE_OVERRIDE,,}" in
|
||||
cascade)
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DCASCADE_PROFILE=1 -DDEFAULT_PATH_HASH_MODE=2 -DDEFAULT_LOOP_DETECT=1 -DDEFAULT_RX_DELAY_BASE=2.0f -DDEFAULT_AGC_RESET_INTERVAL_SECONDS=8 -DDEFAULT_ADVERT_INTERVAL_MINUTES=0 -DDEFAULT_FLOOD_ADVERT_INTERVAL_HOURS=83 -DDEFAULT_MULTI_ACKS=1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
print_build_flags() {
|
||||
local env_name=$1
|
||||
|
||||
@@ -1136,6 +1191,7 @@ build_firmware() {
|
||||
disable_debug_flags
|
||||
apply_debug_overrides
|
||||
apply_radio_overrides
|
||||
apply_firmware_profile_overrides
|
||||
|
||||
print_build_flags "$env_name"
|
||||
pio run -e "$env_name"
|
||||
@@ -1363,6 +1419,7 @@ main() {
|
||||
prompt_for_build_mode
|
||||
prompt_for_debug_build_settings
|
||||
prompt_for_radio_build_settings
|
||||
prompt_for_firmware_profile_settings
|
||||
set -- "${SELECTED_COMMAND_ARGS[@]}"
|
||||
validate_command "$@"
|
||||
fi
|
||||
|
||||
+255
-9
@@ -92,9 +92,10 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
|
||||
---
|
||||
|
||||
### Start an Over-The-Air (OTA) firmware update
|
||||
### Start or stop an Over-The-Air (OTA) firmware update
|
||||
**Usage:**
|
||||
- `start ota`
|
||||
- `stop ota`
|
||||
|
||||
---
|
||||
|
||||
@@ -214,7 +215,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
|
||||
**Parameters:**
|
||||
- `freq`: Frequency in MHz
|
||||
- `bw`: Bandwidth in kHz
|
||||
- `bw`: Bandwidth in kHz. Most targets allow `7.8`, `10.4`, `15.6`, `20.8`, `31.25`, `41.7`, `62.5`, `125`, `250`, `500`. LR1110 targets allow `62.5`, `125`, `250`, `500`.
|
||||
- `sf`: Spreading factor (5-12)
|
||||
- `cr`: Coding rate (5-8)
|
||||
|
||||
@@ -247,8 +248,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
- `tempradio <freq>,<bw>,<sf>,<cr>,<timeout_mins>`
|
||||
|
||||
**Parameters:**
|
||||
- `freq`: Frequency in MHz (300-2500)
|
||||
- `bw`: Bandwidth in kHz (7.8-500)
|
||||
- `freq`: Frequency in MHz (150-2500)
|
||||
- `bw`: Bandwidth in kHz (same allowed values as `set radio`)
|
||||
- `sf`: Spreading factor (5-12)
|
||||
- `cr`: Coding rate (5-8)
|
||||
- `timeout_mins`: Duration in minutes (must be > 0)
|
||||
@@ -257,6 +258,32 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
|
||||
---
|
||||
|
||||
#### Schedule radio parameter changes
|
||||
**Usage:**
|
||||
- `set radioat <freq>,<bw>,<sf>,<cr>,<start_time>`
|
||||
- `get radioat [n|all]`
|
||||
- `del radioat [n|all]`
|
||||
- `set tempradioat <freq>,<bw>,<sf>,<cr>,<start_time>,<end_time>`
|
||||
- `get tempradioat [n|all]`
|
||||
- `del tempradioat [n|all]`
|
||||
|
||||
**Parameters:**
|
||||
- `freq`: Frequency in MHz (150-2500)
|
||||
- `bw`: Bandwidth in kHz (same allowed values as `set radio`)
|
||||
- `sf`: Spreading factor (5-12)
|
||||
- `cr`: Coding rate (5-8)
|
||||
- `start_time`: Unix epoch time when the setting starts
|
||||
- `end_time`: Unix epoch time when a temporary setting reverts
|
||||
- `n`: Scheduled entry number from `get radioat` or `get tempradioat`
|
||||
|
||||
**Notes:**
|
||||
- `get radioat` and `get tempradioat` list all entries when `n` is omitted.
|
||||
- `del radioat` and `del tempradioat` delete all entries when `n` is omitted.
|
||||
- Each queue supports 3 entries. Scheduled entries are not saved across reboot.
|
||||
- `radioat` saves the new radio preferences when it fires. `tempradioat` applies temporarily, then reverts to the saved radio preferences.
|
||||
|
||||
---
|
||||
|
||||
#### View or change this node's frequency
|
||||
**Usage:**
|
||||
- `get freq`
|
||||
@@ -423,6 +450,46 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
|
||||
---
|
||||
|
||||
#### Send a repeater flood text
|
||||
**Usage:**
|
||||
- `send text.flood <message>`
|
||||
|
||||
**Parameters:**
|
||||
- `message`: Text to send to the shared `#repeaters` flood channel, prefixed with this node's name.
|
||||
|
||||
**Example:**
|
||||
```
|
||||
send text.flood checking ridge link
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### View or change battery alert state
|
||||
**Usage:**
|
||||
- `get battery.alert`
|
||||
- `set battery.alert <on|off>`
|
||||
|
||||
**Default:** `off`
|
||||
|
||||
**Note:** When enabled, the repeater checks battery level once per minute and sends low-battery warnings to the `#repeaters` flood channel.
|
||||
|
||||
---
|
||||
|
||||
#### View or change battery alert thresholds
|
||||
**Usage:**
|
||||
- `get battery.alert.low`
|
||||
- `set battery.alert.low <1-100>`
|
||||
- `get battery.alert.critical`
|
||||
- `set battery.alert.critical <0-99>`
|
||||
|
||||
**Defaults:**
|
||||
- `battery.alert.low`: `20`
|
||||
- `battery.alert.critical`: `10`
|
||||
|
||||
**Note:** The low threshold must be greater than the critical threshold.
|
||||
|
||||
---
|
||||
|
||||
#### View this node's public key
|
||||
**Usage:** `get public.key`
|
||||
|
||||
@@ -450,7 +517,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
|
||||
|
||||
**Default:** `off`
|
||||
|
||||
**Note:** When enabled, device enters sleep mode between radio transmissions
|
||||
**Note:** When enabled, device enters sleep mode between radio transmissions. Enabling is refused from the local serial console or while an active USB serial data connection is detected; USB power alone does not block power saving.
|
||||
|
||||
---
|
||||
|
||||
@@ -1011,7 +1078,30 @@ set direct.retry off
|
||||
|
||||
---
|
||||
|
||||
#### View or apply a direct retry preset
|
||||
#### View or change direct retry heard-table gate
|
||||
**Usage:**
|
||||
- `get direct.retry.heard`
|
||||
- `set direct.retry.heard <state>`
|
||||
|
||||
**Parameters:**
|
||||
- `state`: `on`|`off`
|
||||
|
||||
**Default:** `on`
|
||||
|
||||
**Note:** When enabled, the recent repeater table is the direct retry eligibility
|
||||
gate. Prefixes missing from the table are assumed reachable; prefixes in the
|
||||
table below the active SNR gate are blocked.
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
get direct.retry.heard
|
||||
set direct.retry.heard on
|
||||
set direct.retry.heard off
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### View or apply a retry preset
|
||||
**Usage:**
|
||||
- `get retry.preset`
|
||||
- `set retry.preset <preset>`
|
||||
@@ -1020,10 +1110,11 @@ set direct.retry off
|
||||
- `preset`: `infra`|`rooftop`|`mobile`
|
||||
|
||||
**Notes:**
|
||||
- Applies shared direct retry and flood retry defaults.
|
||||
- `infra`: fewer, slower retries for stable fixed infrastructure.
|
||||
- `rooftop`: default long retry window for weak rooftop links.
|
||||
- `mobile`: long retry count with shorter spacing for moving or changing links.
|
||||
- Changing `direct.retry.count`, `direct.retry.base`, `direct.retry.step`, or `direct.retry.margin` makes the preset report as `custom`.
|
||||
- `mobile`: long retry count with shorter spacing for moving or changing links; flood retry count is `15`.
|
||||
- Changing `direct.retry.count`, `direct.retry.base`, `direct.retry.step`, `direct.retry.margin`, `flood.retry.count`, or `flood.retry.path` makes the preset report as `custom`.
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
@@ -1035,6 +1126,155 @@ set retry.preset mobile
|
||||
|
||||
---
|
||||
|
||||
### Flood Retry
|
||||
|
||||
Flood retry resends flood-routed packets when the same packet is not heard from
|
||||
another qualifying repeater.
|
||||
|
||||
#### View or change flood retry count
|
||||
**Usage:**
|
||||
- `get flood.retry.count`
|
||||
- `set flood.retry.count <count>`
|
||||
|
||||
**Parameters:**
|
||||
- `count`: Base retry attempts after the original send, from `0` to `15`. `0` disables flood retry.
|
||||
|
||||
**Note:** Actual attempts are capped at `15`. Hop 1 flood retries use `count * 2`; hop 2 flood retries use `count * 1.5`, rounded up.
|
||||
|
||||
**Defaults:**
|
||||
- `infra`: `1`
|
||||
- `rooftop`: `3`
|
||||
- `mobile`: `15`
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
get flood.retry.count
|
||||
set flood.retry.count 0
|
||||
set flood.retry.count 15
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### View or change flood retry path gate
|
||||
**Usage:**
|
||||
- `get flood.retry.path`
|
||||
- `set flood.retry.path <count|off>`
|
||||
|
||||
**Parameters:**
|
||||
- `count`: Maximum flood path hash count eligible for retry, from `0` to `63`.
|
||||
- `off`: Disable the path-length gate.
|
||||
|
||||
**Defaults:**
|
||||
- `infra`: `1`
|
||||
- `rooftop`: `2`
|
||||
- `mobile`: `1`
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
get flood.retry.path
|
||||
set flood.retry.path 1
|
||||
set flood.retry.path off
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### View or change flood retry advert handling
|
||||
**Usage:**
|
||||
- `get flood.retry.advert`
|
||||
- `set flood.retry.advert <on|off>`
|
||||
|
||||
**Parameters:**
|
||||
- `on`: Retry node advert floods.
|
||||
- `off`: Do not retry node advert floods.
|
||||
|
||||
**Default:** `off`
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
get flood.retry.advert
|
||||
set flood.retry.advert off
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### View or change flood retry target prefixes
|
||||
**Usage:**
|
||||
- `get flood.retry.prefixes`
|
||||
- `set flood.retry.prefixes <prefixes|none|off>`
|
||||
|
||||
**Parameters:**
|
||||
- `prefixes`: Comma-separated 3-byte path hash prefixes, up to 8 entries.
|
||||
- `none` or `off`: Clear the list.
|
||||
|
||||
**Note:** When set, non-bridge flood retry only accepts same-packet echoes whose
|
||||
last hop matches one of these prefixes. When unset, any non-ignored last hop can
|
||||
cancel the retry.
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
get flood.retry.prefixes
|
||||
set flood.retry.prefixes A58296,860CCA,425E5C
|
||||
set flood.retry.prefixes none
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### View or change flood retry ignored prefixes
|
||||
**Usage:**
|
||||
- `get flood.retry.ignore`
|
||||
- `set flood.retry.ignore <prefixes|none|off>`
|
||||
|
||||
**Parameters:**
|
||||
- `prefixes`: Comma-separated 3-byte path hash prefixes, up to 8 entries.
|
||||
- `none` or `off`: Clear the list.
|
||||
|
||||
**Note:** Non-bridge flood retry does not cancel on same-packet echoes whose
|
||||
last hop matches this list. Bridge mode also excludes these prefixes from bucket
|
||||
and `other` hits.
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
get flood.retry.ignore
|
||||
set flood.retry.ignore 71CE82,C7618C
|
||||
set flood.retry.ignore none
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### View or change flood retry bridge mode
|
||||
**Usage:**
|
||||
- `get flood.retry.bridge`
|
||||
- `set flood.retry.bridge <on|off>`
|
||||
|
||||
**Note:** Bridge mode retries until each configured fresh bucket, plus the non-source `other` bucket, has been heard or the retry count is exhausted.
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
get flood.retry.bridge
|
||||
set flood.retry.bridge on
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### View or change flood retry bridge buckets
|
||||
**Usage:**
|
||||
- `get flood.retry.bucket.<n>`
|
||||
- `set flood.retry.bucket <n> <prefixes|none|off>`
|
||||
|
||||
**Parameters:**
|
||||
- `n`: Bucket number from `1` to `6`.
|
||||
- `prefixes`: Comma-separated 3-byte path hash prefixes, up to 17 entries per bucket.
|
||||
- `none` or `off`: Clear the bucket.
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
get flood.retry.bucket.1
|
||||
set flood.retry.bucket 1 71CE82,C7618C
|
||||
set flood.retry.bucket 2 none
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### View or change direct retry count
|
||||
**Usage:**
|
||||
- `get direct.retry.count`
|
||||
@@ -1067,6 +1307,9 @@ set direct.retry.count 15
|
||||
|
||||
**Explanation:**
|
||||
- The first retry waits `base` milliseconds after the failed echo window.
|
||||
- For non-TRACE direct paths shorter than 6 remaining hops, the effective wait is scaled by `hops / 6`.
|
||||
- Non-TRACE direct paths with 6 or more remaining hops use the configured value unchanged.
|
||||
- TRACE retries shorter than 16 remaining hops use `hops / 16`; 16 or more remaining hops use the configured value unchanged.
|
||||
- Larger values reduce channel pressure and give slow repeaters more time.
|
||||
- Smaller values recover faster but create tighter retry bursts.
|
||||
|
||||
@@ -1092,7 +1335,10 @@ set direct.retry.base 500
|
||||
|
||||
**Explanation:**
|
||||
- Retry delay is `base + attempt_index * step`.
|
||||
- With `base=175` and `step=100`, retries wait about `175`, `275`, `375`, `475` ms, and so on.
|
||||
- For non-TRACE direct paths shorter than 6 remaining hops, that computed delay is scaled by `hops / 6`.
|
||||
- Non-TRACE direct paths with 6 or more remaining hops use the computed delay unchanged.
|
||||
- TRACE retries shorter than 16 remaining hops use `hops / 16`; 16 or more remaining hops use the computed delay unchanged.
|
||||
- With `base=175` and `step=100`, non-TRACE paths with 6 or more remaining hops wait about `175`, `275`, `375`, `475` ms, and so on.
|
||||
- `step=0` keeps every retry at the same delay.
|
||||
- Larger steps spread retries over time and are safer on busy channels.
|
||||
|
||||
|
||||
@@ -78,7 +78,6 @@ set flood.retry.ignore none
|
||||
| `battery.alert.critical` | Critical threshold percentage. Critical warnings repeat more often. | `get battery.alert.critical`, `set battery.alert.critical <0-99>` | `set battery.alert.critical 10` |
|
||||
| `recent.repeater` | Shows, seeds, or clears the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. | `get recent.repeater`, `get recent.repeater <page>`, `set recent.repeater <prefix> <snr_db>`, `clear recent.repeater` | `set recent.repeater A1B2C3 -8.5` |
|
||||
| `outpath` | Overrides the primary direct route used for replies to the current remote client. | `get outpath`, `set outpath <hops>`, `set outpath direct`, `set outpath clear`, `set outpath flood` | `set outpath A1B2C3,D4E5F6` |
|
||||
| `altpath` | Optional second direct route used for duplicate response attempts to the current remote client. | `get altpath`, `set altpath <hops>`, `set altpath clear` | `set altpath A1B2C3,D4E5F6` |
|
||||
|
||||
## Other Keymind Commands
|
||||
|
||||
@@ -146,8 +145,8 @@ Serial CLI pages contain up to `128` rows. Remote LoRa CLI pages contain up to
|
||||
|
||||
## Direct Path Overrides
|
||||
|
||||
`outpath` and `altpath` apply to the current remote client ACL entry. They need
|
||||
remote client context, so they are not useful from the local serial CLI.
|
||||
`outpath` applies to the current remote client ACL entry. It needs remote
|
||||
client context, so it is not useful from the local serial CLI.
|
||||
|
||||
Set paths with comma-separated hop hashes. Each hop must be `2`, `4`, or `6`
|
||||
hex characters, and all hops in one path must use the same width.
|
||||
@@ -158,17 +157,12 @@ set outpath A1B2C3,D4E5F6
|
||||
set outpath direct
|
||||
set outpath clear
|
||||
set outpath flood
|
||||
|
||||
get altpath
|
||||
set altpath A1B2C3,D4E5F6
|
||||
set altpath clear
|
||||
```
|
||||
|
||||
`set outpath direct` sets a zero-hop direct route for a client reachable without
|
||||
repeaters. `set outpath clear` forgets the override and lets normal path
|
||||
discovery fill it again. `set outpath flood` forces replies to use flood packets
|
||||
until the client logs in again. `altpath` sends a duplicate reply over a second
|
||||
direct route; clearing it returns replies to a single route.
|
||||
until the client logs in again.
|
||||
|
||||
## Direct Retry Settings
|
||||
|
||||
@@ -180,8 +174,8 @@ Direct retry applies to direct-routed packets. A queued resend is canceled when
|
||||
| `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.base` | Base wait in milliseconds before retry; non-TRACE paths under 6 remaining hops scale by `hops / 6`, TRACE paths under 16 by `hops / 16`. | `get direct.retry.base`, `set direct.retry.base <10-5000>` | `set direct.retry.base 175` |
|
||||
| `direct.retry.step` | Milliseconds added per retry attempt before the same short-path scaling. | `get direct.retry.step`, `set direct.retry.step <0-5000>` | `set direct.retry.step 100` |
|
||||
| `direct.retry.cr` | Adaptive coding-rate thresholds for direct retry packets. Uses `CR4`, `CR5`, `CR7`, or `CR8`; `CR6` is never selected. | `get direct.retry.cr`, `set direct.retry.cr <cr4_min>,<cr5_min>,<cr7_min>,<cr8_max>`, `set direct.retry.cr off` | `set direct.retry.cr 10.0,7.5,2.5,0` |
|
||||
|
||||
The default adaptive coding-rate profile is `10.0,7.5,2.5,2.5`.
|
||||
@@ -217,14 +211,16 @@ 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.
|
||||
Flood retry applies to flood-routed packets. A queued retry is canceled when the
|
||||
same packet is heard from a qualifying, non-ignored repeater. Bridge mode uses
|
||||
the bucket rules below instead.
|
||||
|
||||
| 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.count` | Base flood retry attempts after initial TX. Hop 1 doubles it, hop 2 uses 1.5x rounded up, and actual attempts cap at `15`; `0` disables flood retry. | `get flood.retry.count`, `set flood.retry.count <0-15>` | `set flood.retry.count 7` |
|
||||
| `flood.retry.path` | Maximum path hash count eligible for flood retry, or `off` to disable the gate. | `get flood.retry.path`, `set flood.retry.path <0-63/off>` | `set flood.retry.path 1` |
|
||||
| `flood.retry.advert` | Allows or blocks retry for node advert packets (`type=4`). Default is `off`. | `get flood.retry.advert`, `set flood.retry.advert on/off` | `set flood.retry.advert off` |
|
||||
| `flood.retry.prefixes` | Target prefixes. If set, only matching downstream echoes cancel a retry. | `get flood.retry.prefixes`, `set flood.retry.prefixes <prefixes/none/off>` | `set flood.retry.prefixes BEEBB0,425E5C` |
|
||||
| `flood.retry.prefixes` | Target prefixes. If set, only same-packet echoes from matching last-hop prefixes cancel a retry. | `get flood.retry.prefixes`, `set flood.retry.prefixes <prefixes/none/off>` | `set flood.retry.prefixes BEEBB0,425E5C` |
|
||||
| `flood.retry.ignore` | Ignored prefixes. In non-bridge retry, ignored last-hop echoes do not cancel retry. | `get flood.retry.ignore`, `set flood.retry.ignore <prefixes/none/off>` | `set flood.retry.ignore 71CE82,C7618C` |
|
||||
| `flood.retry.bridge` | Enables bucket-based bridge retry logic. | `get flood.retry.bridge`, `set flood.retry.bridge on/off` | `set flood.retry.bridge on` |
|
||||
| `flood.retry.bucket.<n>` | Shows one bridge bucket. Buckets are numbered `1`-`6`. | `get flood.retry.bucket.<n>` | `get flood.retry.bucket.1` |
|
||||
@@ -236,7 +232,7 @@ The shared retry preset sets these flood defaults:
|
||||
| --- | ---: | ---: |
|
||||
| `infra` | `1` | `1` |
|
||||
| `rooftop` | `3` | `2` |
|
||||
| `mobile` | `3` | `1` |
|
||||
| `mobile` | `15` | `1` |
|
||||
|
||||
Example for path-gated retry:
|
||||
|
||||
|
||||
+1611
-43
File diff suppressed because it is too large
Load Diff
@@ -80,11 +80,32 @@ struct NeighbourInfo {
|
||||
|
||||
#define PACKET_LOG_FILE "/packet_log"
|
||||
|
||||
#ifndef MAX_SCHEDULED_RADIO_SETTINGS_PER_TYPE
|
||||
#define MAX_SCHEDULED_RADIO_SETTINGS_PER_TYPE 3
|
||||
#endif
|
||||
|
||||
#define MAX_SCHEDULED_RADIO_SETTINGS (MAX_SCHEDULED_RADIO_SETTINGS_PER_TYPE * 2)
|
||||
|
||||
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
struct ScheduledRadioSetting {
|
||||
bool active;
|
||||
bool temporary;
|
||||
bool started;
|
||||
float freq;
|
||||
float bw;
|
||||
uint8_t sf;
|
||||
uint8_t cr;
|
||||
uint32_t start_time;
|
||||
uint32_t end_time;
|
||||
};
|
||||
|
||||
FILESYSTEM* _fs;
|
||||
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 +120,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;
|
||||
@@ -107,13 +137,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
NeighbourInfo neighbours[MAX_NEIGHBOURS];
|
||||
#endif
|
||||
CayenneLPP telemetry;
|
||||
unsigned long set_radio_at, revert_radio_at;
|
||||
float pending_freq;
|
||||
float pending_bw;
|
||||
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
|
||||
ScheduledRadioSetting scheduled_radio_settings[MAX_SCHEDULED_RADIO_SETTINGS];
|
||||
int matching_peer_indexes[MAX_CLIENTS];
|
||||
#if defined(WITH_RS232_BRIDGE)
|
||||
RS232Bridge bridge;
|
||||
@@ -126,6 +152,25 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
uint8_t getDirectRetryCodingRateForSNR(int8_t snr_x4) 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);
|
||||
@@ -133,9 +178,30 @@ 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();
|
||||
void printRecentRepeatersSerial();
|
||||
|
||||
File openAppend(const char* fname);
|
||||
bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]);
|
||||
void applyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr);
|
||||
void applySavedRadioParams();
|
||||
void processScheduledRadioSettings();
|
||||
bool isMillisTimerDue(unsigned long timestamp) const;
|
||||
bool hasScheduledRadioWorkDue() const;
|
||||
uint32_t limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_secs) const;
|
||||
uint32_t limitSleepToRtcTime(uint32_t timestamp, uint32_t sleep_secs) const;
|
||||
uint32_t limitSleepToScheduledRadioWork(uint32_t sleep_secs) const;
|
||||
bool hasStartedScheduledTempRadio() const;
|
||||
int findFreeScheduledRadioSlot() const;
|
||||
int countScheduledRadioSettings(bool temporary) const;
|
||||
int findScheduledRadioSettingByIndex(bool temporary, int wanted) const;
|
||||
int getScheduledRadioSettingIndex(bool temporary, int slot_idx) const;
|
||||
bool scheduledRadioConflicts(bool temporary, uint32_t start_time, uint32_t end_time) const;
|
||||
void clearScheduledRadioSetting(int idx, bool restore_if_started);
|
||||
void formatScheduledRadioDuration(char* dest, size_t dest_len, uint32_t target_time) const;
|
||||
void formatRadioParamTuple(char* dest, size_t dest_len, const ScheduledRadioSetting& setting) const;
|
||||
void formatScheduledRadioSetting(char* reply, int setting_idx, int display_idx) const;
|
||||
|
||||
protected:
|
||||
float getAirtimeBudgetFactor() const override {
|
||||
@@ -155,13 +221,22 @@ protected:
|
||||
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;
|
||||
void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt,
|
||||
const uint8_t* target_hash = NULL, uint8_t target_hash_len = 0,
|
||||
int16_t payload_type = -1) override;
|
||||
void onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) override;
|
||||
void onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len, int8_t snr_x4) override;
|
||||
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;
|
||||
@@ -215,6 +290,10 @@ public:
|
||||
|
||||
// CommonCLICallbacks
|
||||
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
|
||||
void addScheduledRadioParams(bool temporary, float freq, float bw, uint8_t sf, uint8_t cr,
|
||||
uint32_t start_time, uint32_t end_time, char* reply) override;
|
||||
void formatScheduledRadioParams(bool temporary, const char* selector, char* reply) override;
|
||||
void deleteScheduledRadioParams(bool temporary, const char* selector, char* reply) override;
|
||||
bool formatFileSystem() override;
|
||||
void sendSelfAdvertisement(int delay_millis, bool flood) override;
|
||||
void updateAdvertTimer() override;
|
||||
@@ -250,6 +329,7 @@ public:
|
||||
handleCommand(sender_timestamp, NULL, command, reply);
|
||||
}
|
||||
void loop();
|
||||
uint32_t getPowerSaveSleepSeconds(uint32_t max_secs) const;
|
||||
|
||||
#if defined(WITH_BRIDGE)
|
||||
void setBridgeState(bool enable) override {
|
||||
|
||||
@@ -152,12 +152,15 @@ void loop() {
|
||||
#endif
|
||||
rtc_clock.tick();
|
||||
|
||||
if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) {
|
||||
if (the_mesh.getNodePrefs()->powersaving_enabled && !board.isUsbDataConnected()) {
|
||||
uint32_t sleep_secs = the_mesh.getPowerSaveSleepSeconds(30);
|
||||
#if defined(NRF52_PLATFORM)
|
||||
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
|
||||
if (sleep_secs > 0) {
|
||||
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
|
||||
}
|
||||
#else
|
||||
if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep
|
||||
board.sleep(30); // Sleep. Wake up after a while or when receiving a LoRa packet
|
||||
if (sleep_secs > 0 && the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep
|
||||
board.sleep(sleep_secs); // Sleep. Wake up for scheduled jobs or when receiving a LoRa packet
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1032,10 +1032,48 @@ void MyMesh::loop() {
|
||||
last_millis = now;
|
||||
}
|
||||
|
||||
bool MyMesh::isMillisTimerDue(unsigned long timestamp) const {
|
||||
return timestamp && millisHasNowPassed(timestamp);
|
||||
}
|
||||
|
||||
uint32_t MyMesh::limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_secs) const {
|
||||
if (!timestamp || sleep_secs == 0) {
|
||||
return sleep_secs;
|
||||
}
|
||||
unsigned long now = millis();
|
||||
if ((long)(now - timestamp) >= 0) {
|
||||
return 0;
|
||||
}
|
||||
unsigned long remaining_ms = timestamp - now;
|
||||
uint32_t remaining_secs = (remaining_ms + 999UL) / 1000UL;
|
||||
return remaining_secs < sleep_secs ? remaining_secs : sleep_secs;
|
||||
}
|
||||
|
||||
uint32_t MyMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const {
|
||||
if (max_secs == 0 || hasPendingWork()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t sleep_secs = max_secs;
|
||||
if (acl.getNumClients() > 0) {
|
||||
sleep_secs = limitSleepToMillisTimer(next_push, sleep_secs);
|
||||
}
|
||||
sleep_secs = limitSleepToMillisTimer(next_flood_advert, sleep_secs);
|
||||
sleep_secs = limitSleepToMillisTimer(next_local_advert, sleep_secs);
|
||||
sleep_secs = limitSleepToMillisTimer(set_radio_at, sleep_secs);
|
||||
sleep_secs = limitSleepToMillisTimer(revert_radio_at, sleep_secs);
|
||||
sleep_secs = limitSleepToMillisTimer(dirty_contacts_expiry, sleep_secs);
|
||||
return sleep_secs;
|
||||
}
|
||||
|
||||
// To check if there is pending work
|
||||
bool MyMesh::hasPendingWork() const {
|
||||
#if defined(WITH_BRIDGE)
|
||||
if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep
|
||||
#endif
|
||||
return _mgr->getOutboundTotal() > 0;
|
||||
if (_mgr->getOutboundTotal() > 0) return true;
|
||||
if (acl.getNumClients() > 0 && isMillisTimerDue(next_push)) return true;
|
||||
if (isMillisTimerDue(next_flood_advert) || isMillisTimerDue(next_local_advert)) return true;
|
||||
if (isMillisTimerDue(set_radio_at) || isMillisTimerDue(revert_radio_at)) return true;
|
||||
return isMillisTimerDue(dirty_contacts_expiry);
|
||||
}
|
||||
|
||||
@@ -225,7 +225,12 @@ public:
|
||||
void clearStats() override;
|
||||
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
|
||||
void loop();
|
||||
uint32_t getPowerSaveSleepSeconds(uint32_t max_secs) const;
|
||||
|
||||
// To check if there is pending work
|
||||
bool hasPendingWork() const;
|
||||
|
||||
private:
|
||||
bool isMillisTimerDue(unsigned long timestamp) const;
|
||||
uint32_t limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_secs) const;
|
||||
};
|
||||
|
||||
@@ -119,12 +119,15 @@ void loop() {
|
||||
#endif
|
||||
rtc_clock.tick();
|
||||
|
||||
if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) {
|
||||
if (the_mesh.getNodePrefs()->powersaving_enabled && !board.isUsbDataConnected()) {
|
||||
uint32_t sleep_secs = the_mesh.getPowerSaveSleepSeconds(30);
|
||||
#if defined(NRF52_PLATFORM)
|
||||
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
|
||||
if (sleep_secs > 0) {
|
||||
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
|
||||
}
|
||||
#else
|
||||
if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep
|
||||
board.sleep(30); // Sleep. Wake up after a while or when receiving a LoRa packet
|
||||
if (sleep_secs > 0 && the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep
|
||||
board.sleep(sleep_secs); // Sleep. Wake up for scheduled jobs or when receiving a LoRa packet
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
+85
-24
@@ -28,6 +28,41 @@ static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) {
|
||||
return size_linear;
|
||||
}
|
||||
|
||||
static uint8_t getTraceRemainingHops(const Packet* packet) {
|
||||
if (packet == NULL || packet->payload_len < 9) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t route_bytes = packet->payload_len - 9;
|
||||
uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes);
|
||||
if (hash_size == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t route_hops = route_bytes / hash_size;
|
||||
if (packet->path_len >= route_hops) {
|
||||
return 0;
|
||||
}
|
||||
return route_hops - packet->path_len;
|
||||
}
|
||||
|
||||
static uint8_t getTraceDirectPriority(const Packet* packet) {
|
||||
uint8_t remaining_hops = getTraceRemainingHops(packet);
|
||||
if (remaining_hops == 0) {
|
||||
return 5;
|
||||
}
|
||||
if (remaining_hops <= 4) {
|
||||
return 1;
|
||||
}
|
||||
if (remaining_hops <= 8) {
|
||||
return 2;
|
||||
}
|
||||
if (remaining_hops <= 12) {
|
||||
return 3;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
void Mesh::begin() {
|
||||
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
|
||||
_direct_retries[i].packet = NULL;
|
||||
@@ -39,6 +74,7 @@ void Mesh::begin() {
|
||||
_direct_retries[i].retry_attempts_sent = 0;
|
||||
memset(_direct_retries[i].next_hop_hash, 0, sizeof(_direct_retries[i].next_hop_hash));
|
||||
_direct_retries[i].next_hop_hash_len = 0;
|
||||
_direct_retries[i].payload_type = 0;
|
||||
_direct_retries[i].priority = 0;
|
||||
_direct_retries[i].progress_marker = 0;
|
||||
_direct_retries[i].expect_path_growth = false;
|
||||
@@ -78,8 +114,12 @@ void Mesh::loop() {
|
||||
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);
|
||||
onDirectRetryEvent("failed_all_tries", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len,
|
||||
_direct_retries[i].payload_type);
|
||||
onDirectRetryEvent("failure", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len,
|
||||
_direct_retries[i].payload_type);
|
||||
onDirectRetryFailed(_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
clearDirectRetrySlot(i);
|
||||
continue;
|
||||
@@ -230,9 +270,10 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
|
||||
// append SNR (Not hash!)
|
||||
pkt->path[pkt->path_len++] = (int8_t) (pkt->getSNR()*4);
|
||||
|
||||
uint8_t pri = getTraceDirectPriority(pkt);
|
||||
uint32_t d = getDirectRetransmitDelay(pkt);
|
||||
maybeScheduleDirectRetry(pkt, 5);
|
||||
return ACTION_RETRANSMIT_DELAYED(5, d); // schedule with priority 5 (for now), maybe make configurable?
|
||||
maybeScheduleDirectRetry(pkt, pri);
|
||||
return ACTION_RETRANSMIT_DELAYED(pri, d);
|
||||
}
|
||||
}
|
||||
return ACTION_RELEASE;
|
||||
@@ -589,6 +630,7 @@ void Mesh::clearDirectRetrySlot(int idx) {
|
||||
_direct_retries[idx].retry_attempts_sent = 0;
|
||||
memset(_direct_retries[idx].next_hop_hash, 0, sizeof(_direct_retries[idx].next_hop_hash));
|
||||
_direct_retries[idx].next_hop_hash_len = 0;
|
||||
_direct_retries[idx].payload_type = 0;
|
||||
_direct_retries[idx].priority = 0;
|
||||
_direct_retries[idx].progress_marker = 0;
|
||||
_direct_retries[idx].expect_path_growth = false;
|
||||
@@ -641,7 +683,9 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) {
|
||||
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);
|
||||
onDirectRetryEvent("good", _direct_retries[i].packet, echo_millis, retry_attempt,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len,
|
||||
_direct_retries[i].payload_type);
|
||||
if (_direct_retries[i].queued) {
|
||||
for (int j = 0; j < _mgr->getOutboundTotal(); j++) {
|
||||
if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) {
|
||||
@@ -661,7 +705,8 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) {
|
||||
uint32_t echo_millis = _direct_retries[i].echo_wait_started_at == 0
|
||||
? 0
|
||||
: (uint32_t)(_ms->getMillis() - _direct_retries[i].echo_wait_started_at);
|
||||
onDirectRetryEvent("good", _direct_retries[i].trigger_packet, echo_millis, _direct_retries[i].retry_attempts_sent + 1);
|
||||
onDirectRetryEvent("good", _direct_retries[i].trigger_packet, echo_millis, _direct_retries[i].retry_attempts_sent + 1,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
clearDirectRetrySlot(i);
|
||||
}
|
||||
cleared = true;
|
||||
@@ -682,7 +727,8 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) {
|
||||
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);
|
||||
onDirectRetryEvent("resent", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
_direct_retries[i].echo_wait_started_at = _ms->getMillis();
|
||||
_direct_retries[i].retry_attempts_sent++;
|
||||
uint8_t max_attempts = getDirectRetryMaxAttempts(packet);
|
||||
@@ -703,8 +749,10 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) {
|
||||
|
||||
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);
|
||||
onDirectRetryEvent("dropped_no_packet", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
clearDirectRetrySlot(i);
|
||||
continue;
|
||||
}
|
||||
@@ -719,10 +767,13 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) {
|
||||
_direct_retries[i].retry_delay = retry_delay;
|
||||
_direct_retries[i].retry_at = futureMillis(retry_delay);
|
||||
_direct_retries[i].waiting_final_echo = false;
|
||||
onDirectRetryEvent("queued", retry, retry_delay, retry_attempt);
|
||||
onDirectRetryEvent("queued", retry, retry_delay, retry_attempt,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
} else {
|
||||
onDirectRetryEvent("dropped_queue_full", retry, retry_delay, retry_attempt);
|
||||
onDirectRetryEvent("failure", retry, elapsed_millis, retry_attempt);
|
||||
onDirectRetryEvent("dropped_queue_full", retry, retry_delay, retry_attempt,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
onDirectRetryEvent("failure", retry, elapsed_millis, retry_attempt,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
releasePacket(retry);
|
||||
clearDirectRetrySlot(i);
|
||||
}
|
||||
@@ -737,8 +788,10 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) {
|
||||
// Allocate the retry packet only after TX-complete so busy repeaters do not reserve pool slots early.
|
||||
Packet* retry = obtainNewPacket();
|
||||
if (retry == NULL) {
|
||||
onDirectRetryEvent("dropped_no_packet", packet, _direct_retries[i].retry_delay, 1);
|
||||
onDirectRetryEvent("failure", packet, 0, 1);
|
||||
onDirectRetryEvent("dropped_no_packet", packet, _direct_retries[i].retry_delay, 1,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
onDirectRetryEvent("failure", packet, 0, 1,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
clearDirectRetrySlot(i);
|
||||
continue;
|
||||
}
|
||||
@@ -757,10 +810,13 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) {
|
||||
_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);
|
||||
onDirectRetryEvent("queued", retry, _direct_retries[i].retry_delay, 1,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
} else {
|
||||
onDirectRetryEvent("dropped_queue_full", retry, _direct_retries[i].retry_delay, 1);
|
||||
onDirectRetryEvent("failure", retry, 0, 1);
|
||||
onDirectRetryEvent("dropped_queue_full", retry, _direct_retries[i].retry_delay, 1,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
onDirectRetryEvent("failure", retry, 0, 1,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
releasePacket(retry);
|
||||
clearDirectRetrySlot(i);
|
||||
}
|
||||
@@ -776,16 +832,20 @@ void Mesh::clearPendingDirectRetryOnSendFail(const Packet* packet) {
|
||||
if (_direct_retries[i].queued) {
|
||||
if (_direct_retries[i].packet == packet) {
|
||||
// The queued retry itself failed; Dispatcher will release it after this hook.
|
||||
onDirectRetryEvent("dropped_send_fail", packet, 0, _direct_retries[i].retry_attempts_sent + 1);
|
||||
onDirectRetryEvent("failure", packet, 0, _direct_retries[i].retry_attempts_sent + 1);
|
||||
onDirectRetryEvent("dropped_send_fail", packet, 0, _direct_retries[i].retry_attempts_sent + 1,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
onDirectRetryEvent("failure", packet, 0, _direct_retries[i].retry_attempts_sent + 1,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
clearDirectRetrySlot(i);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_direct_retries[i].trigger_packet == packet) {
|
||||
onDirectRetryEvent("dropped_send_fail", packet, 0, 1);
|
||||
onDirectRetryEvent("failure", packet, 0, 1);
|
||||
onDirectRetryEvent("dropped_send_fail", packet, 0, 1,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
onDirectRetryEvent("failure", packet, 0, 1,
|
||||
_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
|
||||
clearDirectRetrySlot(i);
|
||||
}
|
||||
}
|
||||
@@ -922,8 +982,8 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) {
|
||||
}
|
||||
}
|
||||
if (slot_idx < 0) {
|
||||
onDirectRetryEvent("dropped_no_slot", packet, 0, 0);
|
||||
onDirectRetryEvent("failure", packet, 0, 0);
|
||||
onDirectRetryEvent("dropped_no_slot", packet, 0, 0, next_hop_hash, next_hop_hash_len);
|
||||
onDirectRetryEvent("failure", packet, 0, 0, next_hop_hash, next_hop_hash_len);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -940,6 +1000,7 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) {
|
||||
memset(_direct_retries[slot_idx].next_hop_hash, 0, sizeof(_direct_retries[slot_idx].next_hop_hash));
|
||||
memcpy(_direct_retries[slot_idx].next_hop_hash, next_hop_hash, next_hop_hash_len);
|
||||
_direct_retries[slot_idx].next_hop_hash_len = next_hop_hash_len;
|
||||
_direct_retries[slot_idx].payload_type = packet->getPayloadType();
|
||||
_direct_retries[slot_idx].priority = priority;
|
||||
_direct_retries[slot_idx].progress_marker = progress_marker;
|
||||
_direct_retries[slot_idx].expect_path_growth = expect_path_growth;
|
||||
@@ -1496,7 +1557,7 @@ void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uin
|
||||
packet->payload_len += path_len;
|
||||
|
||||
packet->path_len = 0;
|
||||
pri = 5; // maybe make this configurable
|
||||
pri = getTraceDirectPriority(packet);
|
||||
} else {
|
||||
packet->path_len = Packet::copyPath(packet->path, path, path_len);
|
||||
if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
|
||||
|
||||
+4
-1
@@ -48,6 +48,7 @@ class Mesh : public Dispatcher {
|
||||
uint8_t retry_key[MAX_HASH_SIZE];
|
||||
uint8_t next_hop_hash[MAX_HASH_SIZE];
|
||||
uint8_t next_hop_hash_len;
|
||||
uint8_t payload_type;
|
||||
uint8_t priority;
|
||||
uint8_t progress_marker;
|
||||
bool expect_path_growth;
|
||||
@@ -201,7 +202,9 @@ protected:
|
||||
/**
|
||||
* \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) { }
|
||||
virtual void onDirectRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt,
|
||||
const uint8_t* target_hash = NULL, uint8_t target_hash_len = 0,
|
||||
int16_t payload_type = -1) { }
|
||||
|
||||
/**
|
||||
* \brief Optional hook for link-quality feedback when all direct-retry attempts fail.
|
||||
|
||||
@@ -64,12 +64,14 @@ public:
|
||||
virtual uint8_t getStartupReason() const = 0;
|
||||
virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; }
|
||||
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
|
||||
virtual bool stopOTAUpdate(char reply[]) { return false; } // not supported
|
||||
virtual bool setLoRaFemLnaEnabled(bool enable) { return false; }
|
||||
virtual bool canControlLoRaFemLna() const { return false; }
|
||||
virtual bool isLoRaFemLnaEnabled() const { return false; }
|
||||
|
||||
// Power management interface (boards with power management override these)
|
||||
virtual bool isExternalPowered() { return false; }
|
||||
virtual bool isUsbDataConnected() { return false; }
|
||||
virtual uint16_t getBootVoltage() { return 0; }
|
||||
virtual uint32_t getResetReason() const { return 0; }
|
||||
virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; }
|
||||
|
||||
+483
-12
@@ -95,6 +95,150 @@ static bool looksUnsignedInteger(const char* s) {
|
||||
return saw_digit;
|
||||
}
|
||||
|
||||
static bool parseUint8Strict(const char* value, uint8_t min_value, uint8_t max_value, uint8_t& result) {
|
||||
if (value == NULL || *value == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t parsed = 0;
|
||||
const char* sp = value;
|
||||
while (*sp) {
|
||||
if (*sp < '0' || *sp > '9') {
|
||||
return false;
|
||||
}
|
||||
parsed = (uint16_t)((parsed * 10) + (*sp - '0'));
|
||||
if (parsed > max_value) {
|
||||
return false;
|
||||
}
|
||||
sp++;
|
||||
}
|
||||
if (parsed < min_value) {
|
||||
return false;
|
||||
}
|
||||
result = (uint8_t)parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool bwMatches(float bw, float allowed) {
|
||||
float diff = bw - allowed;
|
||||
if (diff < 0.0f) diff = -diff;
|
||||
return diff <= 0.001f;
|
||||
}
|
||||
|
||||
static bool isValidLoRaBandwidth(float bw) {
|
||||
#if defined(USE_LR1110)
|
||||
return bwMatches(bw, 62.5f)
|
||||
|| bwMatches(bw, 125.0f)
|
||||
|| bwMatches(bw, 250.0f)
|
||||
|| bwMatches(bw, 500.0f);
|
||||
#elif defined(USE_LLCC68) || defined(USE_SX1272)
|
||||
return bwMatches(bw, 125.0f)
|
||||
|| bwMatches(bw, 250.0f)
|
||||
|| bwMatches(bw, 500.0f);
|
||||
#else
|
||||
return bwMatches(bw, 7.8f)
|
||||
|| bwMatches(bw, 10.4f)
|
||||
|| bwMatches(bw, 15.6f)
|
||||
|| bwMatches(bw, 20.8f)
|
||||
|| bwMatches(bw, 31.25f)
|
||||
|| bwMatches(bw, 41.7f)
|
||||
|| bwMatches(bw, 62.5f)
|
||||
|| bwMatches(bw, 125.0f)
|
||||
|| bwMatches(bw, 250.0f)
|
||||
|| bwMatches(bw, 500.0f);
|
||||
#endif
|
||||
}
|
||||
|
||||
static float defaultLoRaBandwidth() {
|
||||
#ifdef LORA_BW
|
||||
if (isValidLoRaBandwidth((float)LORA_BW)) {
|
||||
return (float)LORA_BW;
|
||||
}
|
||||
#endif
|
||||
return 125.0f;
|
||||
}
|
||||
|
||||
static const char* skipSpacesConst(const char* s) {
|
||||
while (s != NULL && *s == ' ') s++;
|
||||
return s;
|
||||
}
|
||||
|
||||
static bool parseUint32Strict(const char* s, uint32_t& out) {
|
||||
if (!looksUnsignedInteger(s)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t n = 0;
|
||||
s = skipSpacesConst(s);
|
||||
while (*s >= '0' && *s <= '9') {
|
||||
n = (n * 10) + (uint32_t)(*s - '0');
|
||||
if (n > 0xFFFFFFFFULL) {
|
||||
return false;
|
||||
}
|
||||
s++;
|
||||
}
|
||||
out = (uint32_t)n;
|
||||
return true;
|
||||
}
|
||||
|
||||
static int countSeparatedParts(const char* s, char separator) {
|
||||
if (s == NULL || *s == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 1;
|
||||
while (*s) {
|
||||
if (*s++ == separator) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static bool parseScheduledRadioArgs(const char* args, bool temporary, float& freq, float& bw,
|
||||
uint8_t& sf, uint8_t& cr, uint32_t& start_time,
|
||||
uint32_t& end_time) {
|
||||
const int expected_parts = temporary ? 6 : 5;
|
||||
args = skipSpacesConst(args);
|
||||
if (countSeparatedParts(args, ',') != expected_parts) {
|
||||
return false;
|
||||
}
|
||||
char local[96];
|
||||
if (strlen(args) >= sizeof(local)) {
|
||||
return false;
|
||||
}
|
||||
StrHelper::strncpy(local, args, sizeof(local));
|
||||
const char* parts[6];
|
||||
int num = mesh::Utils::parseTextParts(local, parts, expected_parts, ',');
|
||||
if (num != expected_parts) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t sf_u32 = 0;
|
||||
uint32_t cr_u32 = 0;
|
||||
if (!looksNumeric(parts[0]) || !looksNumeric(parts[1])
|
||||
|| !parseUint32Strict(parts[2], sf_u32)
|
||||
|| !parseUint32Strict(parts[3], cr_u32)
|
||||
|| !parseUint32Strict(parts[4], start_time)) {
|
||||
return false;
|
||||
}
|
||||
if (sf_u32 > 255 || cr_u32 > 255) {
|
||||
return false;
|
||||
}
|
||||
|
||||
freq = atof(parts[0]);
|
||||
bw = atof(parts[1]);
|
||||
sf = (uint8_t)sf_u32;
|
||||
cr = (uint8_t)cr_u32;
|
||||
if (temporary && !parseUint32Strict(parts[5], end_time)) {
|
||||
return false;
|
||||
}
|
||||
if (!temporary) {
|
||||
end_time = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int16_t parseSnrDbX4(const char* s) {
|
||||
float db = atof(s);
|
||||
return (int16_t)(db * 4.0f + (db >= 0.0f ? 0.5f : -0.5f));
|
||||
@@ -124,6 +268,93 @@ static void markDirectRetryPrefsValid(NodePrefs* prefs) {
|
||||
prefs->direct_retry_prefs_magic[1] = DIRECT_RETRY_PREFS_MAGIC_1;
|
||||
}
|
||||
|
||||
static void applyFloodRetryPreset(NodePrefs* prefs, uint8_t preset) {
|
||||
if (preset == RETRY_PRESET_INFRA) {
|
||||
prefs->flood_retry_attempts = FLOOD_RETRY_INFRA_COUNT;
|
||||
prefs->flood_retry_max_path = FLOOD_RETRY_INFRA_MAX_PATH;
|
||||
} else if (preset == RETRY_PRESET_MOBILE) {
|
||||
prefs->flood_retry_attempts = FLOOD_RETRY_MOBILE_COUNT;
|
||||
prefs->flood_retry_max_path = FLOOD_RETRY_MOBILE_MAX_PATH;
|
||||
} else {
|
||||
prefs->flood_retry_attempts = FLOOD_RETRY_ROOFTOP_COUNT;
|
||||
prefs->flood_retry_max_path = FLOOD_RETRY_ROOFTOP_MAX_PATH;
|
||||
}
|
||||
}
|
||||
|
||||
static bool parseFloodRetryPathGate(const char* value, uint8_t& path_gate) {
|
||||
if (value == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (strcmp(value, "off") == 0 || strcmp(value, "disabled") == 0 || strcmp(value, "disable") == 0) {
|
||||
path_gate = FLOOD_RETRY_PATH_GATE_DISABLED;
|
||||
return true;
|
||||
}
|
||||
return parseUint8Strict(value, 0, 63, path_gate);
|
||||
}
|
||||
|
||||
static void formatFloodRetryPathGate(char* dest, uint8_t path_gate) {
|
||||
if (path_gate == FLOOD_RETRY_PATH_GATE_DISABLED) {
|
||||
strcpy(dest, "off");
|
||||
} else {
|
||||
sprintf(dest, "%u", (unsigned int)path_gate);
|
||||
}
|
||||
}
|
||||
|
||||
static void formatFloodRetryPrefixList(char* dest, const uint8_t prefixes[][FLOOD_RETRY_PREFIX_LEN],
|
||||
uint8_t max_prefixes) {
|
||||
char* out = dest;
|
||||
bool first = true;
|
||||
for (int i = 0; i < max_prefixes; i++) {
|
||||
const uint8_t* prefix = prefixes[i];
|
||||
if (prefix[0] == 0 && prefix[1] == 0 && prefix[2] == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!first) {
|
||||
*out++ = ',';
|
||||
}
|
||||
mesh::Utils::toHex(out, prefix, FLOOD_RETRY_PREFIX_LEN);
|
||||
out += FLOOD_RETRY_PREFIX_LEN * 2;
|
||||
first = false;
|
||||
}
|
||||
*out = 0;
|
||||
}
|
||||
|
||||
static bool parseFloodRetryPrefixList(uint8_t dest[][FLOOD_RETRY_PREFIX_LEN], uint8_t max_prefixes, const char* value) {
|
||||
if (max_prefixes > FLOOD_RETRY_LIST_PREFIXES) {
|
||||
return false;
|
||||
}
|
||||
uint8_t parsed[FLOOD_RETRY_LIST_PREFIXES][FLOOD_RETRY_PREFIX_LEN];
|
||||
memset(parsed, 0, sizeof(parsed));
|
||||
if (value == NULL || value[0] == 0 || strcmp(value, "none") == 0 || strcmp(value, "off") == 0) {
|
||||
memcpy(dest, parsed, max_prefixes * FLOOD_RETRY_PREFIX_LEN);
|
||||
return true;
|
||||
}
|
||||
|
||||
char local[FLOOD_RETRY_LIST_TEXT_MAX];
|
||||
StrHelper::strncpy(local, value, sizeof(local));
|
||||
const char* parts[FLOOD_RETRY_LIST_PREFIXES + 1];
|
||||
int num = mesh::Utils::parseTextParts(local, parts, FLOOD_RETRY_LIST_PREFIXES + 1);
|
||||
if (num > max_prefixes) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < num; i++) {
|
||||
if (strlen(parts[i]) != FLOOD_RETRY_PREFIX_LEN * 2) {
|
||||
return false;
|
||||
}
|
||||
for (int j = 0; j < FLOOD_RETRY_PREFIX_LEN * 2; j++) {
|
||||
if (!mesh::Utils::isHexChar(parts[i][j])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!mesh::Utils::fromHex(parsed[i], FLOOD_RETRY_PREFIX_LEN, parts[i])
|
||||
|| (parsed[i][0] == 0 && parsed[i][1] == 0 && parsed[i][2] == 0)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
memcpy(dest, parsed, max_prefixes * FLOOD_RETRY_PREFIX_LEN);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) {
|
||||
prefs->retry_preset = preset;
|
||||
if (preset == RETRY_PRESET_INFRA) {
|
||||
@@ -143,6 +374,7 @@ static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) {
|
||||
prefs->direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS;
|
||||
prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_ROOFTOP_MARGIN_X4;
|
||||
}
|
||||
applyFloodRetryPreset(prefs, prefs->retry_preset);
|
||||
markDirectRetryPrefsValid(prefs);
|
||||
}
|
||||
|
||||
@@ -154,6 +386,7 @@ static void setDefaultDirectRetryPrefs(NodePrefs* prefs) {
|
||||
prefs->direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT;
|
||||
prefs->direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT;
|
||||
prefs->direct_retry_enabled = 1;
|
||||
prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT;
|
||||
markDirectRetryPrefsValid(prefs);
|
||||
}
|
||||
|
||||
@@ -280,7 +513,48 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
file.read((uint8_t *)&_prefs->direct_retry_enabled, sizeof(_prefs->direct_retry_enabled)); // 307
|
||||
file.read((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 308
|
||||
file.read((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 309
|
||||
// next: 311
|
||||
memset(_prefs->flood_retry_prefixes, 0, sizeof(_prefs->flood_retry_prefixes));
|
||||
_prefs->flood_retry_bridge_enabled = 0;
|
||||
memset(_prefs->flood_retry_bridge_buckets, 0, sizeof(_prefs->flood_retry_bridge_buckets));
|
||||
memset(_prefs->flood_retry_ignore_prefixes, 0, sizeof(_prefs->flood_retry_ignore_prefixes));
|
||||
_prefs->flood_retry_advert_enabled = FLOOD_RETRY_ADVERT_DEFAULT;
|
||||
_prefs->battery_alert_enabled = 0;
|
||||
_prefs->battery_alert_low_percent = BATTERY_ALERT_LOW_PERCENT_DEFAULT;
|
||||
_prefs->battery_alert_critical_percent = BATTERY_ALERT_CRITICAL_PERCENT_DEFAULT;
|
||||
_prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT;
|
||||
bool has_flood_retry_prefs = file.available() >= 2;
|
||||
if (has_flood_retry_prefs) {
|
||||
file.read((uint8_t *)&_prefs->flood_retry_attempts, sizeof(_prefs->flood_retry_attempts)); // 311
|
||||
file.read((uint8_t *)&_prefs->flood_retry_max_path, sizeof(_prefs->flood_retry_max_path)); // 312
|
||||
if (file.available() >= (int)sizeof(_prefs->flood_retry_prefixes)) {
|
||||
file.read((uint8_t *)&_prefs->flood_retry_prefixes[0][0], sizeof(_prefs->flood_retry_prefixes));
|
||||
}
|
||||
if (file.available() >= (int)sizeof(_prefs->flood_retry_bridge_enabled)) {
|
||||
file.read((uint8_t *)&_prefs->flood_retry_bridge_enabled, sizeof(_prefs->flood_retry_bridge_enabled));
|
||||
}
|
||||
if (file.available() >= (int)sizeof(_prefs->flood_retry_bridge_buckets)) {
|
||||
file.read((uint8_t *)&_prefs->flood_retry_bridge_buckets[0][0][0], sizeof(_prefs->flood_retry_bridge_buckets));
|
||||
}
|
||||
if (file.available() >= (int)sizeof(_prefs->flood_retry_ignore_prefixes)) {
|
||||
file.read((uint8_t *)&_prefs->flood_retry_ignore_prefixes[0][0], sizeof(_prefs->flood_retry_ignore_prefixes));
|
||||
}
|
||||
if (file.available() >= (int)sizeof(_prefs->flood_retry_advert_enabled)) {
|
||||
file.read((uint8_t *)&_prefs->flood_retry_advert_enabled, sizeof(_prefs->flood_retry_advert_enabled));
|
||||
}
|
||||
if (file.available() >= (int)sizeof(_prefs->battery_alert_enabled)) {
|
||||
file.read((uint8_t *)&_prefs->battery_alert_enabled, sizeof(_prefs->battery_alert_enabled));
|
||||
}
|
||||
if (file.available() >= (int)sizeof(_prefs->battery_alert_low_percent)) {
|
||||
file.read((uint8_t *)&_prefs->battery_alert_low_percent, sizeof(_prefs->battery_alert_low_percent));
|
||||
}
|
||||
if (file.available() >= (int)sizeof(_prefs->battery_alert_critical_percent)) {
|
||||
file.read((uint8_t *)&_prefs->battery_alert_critical_percent, sizeof(_prefs->battery_alert_critical_percent));
|
||||
}
|
||||
if (file.available() >= (int)sizeof(_prefs->direct_retry_recent_enabled)) {
|
||||
file.read((uint8_t *)&_prefs->direct_retry_recent_enabled, sizeof(_prefs->direct_retry_recent_enabled));
|
||||
}
|
||||
}
|
||||
// next: 672
|
||||
|
||||
// sanitise bad pref values
|
||||
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
|
||||
@@ -288,7 +562,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
_prefs->direct_tx_delay_factor = constrain(_prefs->direct_tx_delay_factor, 0, 2.0f);
|
||||
_prefs->airtime_factor = constrain(_prefs->airtime_factor, 0, 9.0f);
|
||||
_prefs->freq = constrain(_prefs->freq, 150.0f, 2500.0f);
|
||||
_prefs->bw = constrain(_prefs->bw, 7.8f, 500.0f);
|
||||
_prefs->bw = isValidLoRaBandwidth(_prefs->bw) ? _prefs->bw : defaultLoRaBandwidth();
|
||||
_prefs->sf = constrain(_prefs->sf, 5, 12);
|
||||
_prefs->cr = constrain(_prefs->cr, 5, 8);
|
||||
_prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, -9, 30);
|
||||
@@ -315,6 +589,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
_prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean
|
||||
if (!directRetryPrefsValid(_prefs)) {
|
||||
setDefaultDirectRetryPrefs(_prefs);
|
||||
memset(_prefs->flood_retry_prefixes, 0, sizeof(_prefs->flood_retry_prefixes));
|
||||
_prefs->flood_retry_bridge_enabled = 0;
|
||||
memset(_prefs->flood_retry_bridge_buckets, 0, sizeof(_prefs->flood_retry_bridge_buckets));
|
||||
memset(_prefs->flood_retry_ignore_prefixes, 0, sizeof(_prefs->flood_retry_ignore_prefixes));
|
||||
_prefs->flood_retry_advert_enabled = FLOOD_RETRY_ADVERT_DEFAULT;
|
||||
} else if (!has_flood_retry_prefs) {
|
||||
applyFloodRetryPreset(_prefs, _prefs->retry_preset);
|
||||
}
|
||||
if (_prefs->retry_preset > RETRY_PRESET_MOBILE && _prefs->retry_preset != RETRY_PRESET_CUSTOM) {
|
||||
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
|
||||
@@ -325,6 +606,20 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
_prefs->direct_retry_snr_margin_x4 = constrain(_prefs->direct_retry_snr_margin_x4, 0, 160);
|
||||
_prefs->direct_retry_enabled = constrain(_prefs->direct_retry_enabled, 0, 1);
|
||||
_prefs->direct_retry_cr_enabled = constrain(_prefs->direct_retry_cr_enabled, 0, 1);
|
||||
_prefs->flood_retry_attempts = constrain(_prefs->flood_retry_attempts, 0, 15);
|
||||
if (_prefs->flood_retry_max_path != FLOOD_RETRY_PATH_GATE_DISABLED) {
|
||||
_prefs->flood_retry_max_path = constrain(_prefs->flood_retry_max_path, 0, 63);
|
||||
}
|
||||
_prefs->flood_retry_bridge_enabled = constrain(_prefs->flood_retry_bridge_enabled, 0, 1);
|
||||
_prefs->flood_retry_advert_enabled = constrain(_prefs->flood_retry_advert_enabled, 0, 1);
|
||||
_prefs->battery_alert_enabled = constrain(_prefs->battery_alert_enabled, 0, 1);
|
||||
_prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1);
|
||||
if (_prefs->battery_alert_low_percent < 1
|
||||
|| _prefs->battery_alert_low_percent > 100
|
||||
|| _prefs->battery_alert_critical_percent >= _prefs->battery_alert_low_percent) {
|
||||
_prefs->battery_alert_low_percent = BATTERY_ALERT_LOW_PERCENT_DEFAULT;
|
||||
_prefs->battery_alert_critical_percent = BATTERY_ALERT_CRITICAL_PERCENT_DEFAULT;
|
||||
}
|
||||
|
||||
file.close();
|
||||
}
|
||||
@@ -405,7 +700,18 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
|
||||
file.write((uint8_t *)&_prefs->direct_retry_enabled, sizeof(_prefs->direct_retry_enabled)); // 307
|
||||
file.write((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 308
|
||||
file.write((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 309
|
||||
// next: 311
|
||||
file.write((uint8_t *)&_prefs->flood_retry_attempts, sizeof(_prefs->flood_retry_attempts)); // 311
|
||||
file.write((uint8_t *)&_prefs->flood_retry_max_path, sizeof(_prefs->flood_retry_max_path)); // 312
|
||||
file.write((uint8_t *)&_prefs->flood_retry_prefixes[0][0], sizeof(_prefs->flood_retry_prefixes)); // 313
|
||||
file.write((uint8_t *)&_prefs->flood_retry_bridge_enabled, sizeof(_prefs->flood_retry_bridge_enabled));
|
||||
file.write((uint8_t *)&_prefs->flood_retry_bridge_buckets[0][0][0], sizeof(_prefs->flood_retry_bridge_buckets));
|
||||
file.write((uint8_t *)&_prefs->flood_retry_ignore_prefixes[0][0], sizeof(_prefs->flood_retry_ignore_prefixes));
|
||||
file.write((uint8_t *)&_prefs->flood_retry_advert_enabled, sizeof(_prefs->flood_retry_advert_enabled));
|
||||
file.write((uint8_t *)&_prefs->battery_alert_enabled, sizeof(_prefs->battery_alert_enabled));
|
||||
file.write((uint8_t *)&_prefs->battery_alert_low_percent, sizeof(_prefs->battery_alert_low_percent));
|
||||
file.write((uint8_t *)&_prefs->battery_alert_critical_percent, sizeof(_prefs->battery_alert_critical_percent));
|
||||
file.write((uint8_t *)&_prefs->direct_retry_recent_enabled, sizeof(_prefs->direct_retry_recent_enabled));
|
||||
// next: 672
|
||||
|
||||
file.close();
|
||||
}
|
||||
@@ -466,10 +772,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
} else {
|
||||
strcpy(reply, "ERR: clock cannot go backwards");
|
||||
}
|
||||
} else if (memcmp(command, "start ota", 9) == 0) {
|
||||
} else if (memcmp(command, "start ota", 9) == 0 && (command[9] == 0 || command[9] == ' ')) {
|
||||
if (!_board->startOTAUpdate(_prefs->node_name, reply)) {
|
||||
strcpy(reply, "Error");
|
||||
}
|
||||
} else if (memcmp(command, "stop ota", 8) == 0 && (command[8] == 0 || command[8] == ' ')) {
|
||||
if (!_board->stopOTAUpdate(reply)) {
|
||||
strcpy(reply, "Error");
|
||||
}
|
||||
} else if (memcmp(command, "clock", 5) == 0) {
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
DateTime dt = DateTime(now);
|
||||
@@ -507,7 +817,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
uint8_t sf = num > 2 ? atoi(parts[2]) : 0;
|
||||
uint8_t cr = num > 3 ? atoi(parts[3]) : 0;
|
||||
int temp_timeout_mins = num > 4 ? atoi(parts[4]) : 0;
|
||||
if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f && temp_timeout_mins > 0) {
|
||||
if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && isValidLoRaBandwidth(bw) && temp_timeout_mins > 0) {
|
||||
_callbacks->applyTempRadioParams(freq, bw, sf, cr, temp_timeout_mins);
|
||||
sprintf(reply, "OK - temp params for %d mins", temp_timeout_mins);
|
||||
} else {
|
||||
@@ -529,6 +839,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
handleGetCmd(sender_timestamp, command, reply);
|
||||
} else if (memcmp(command, "set ", 4) == 0) {
|
||||
handleSetCmd(sender_timestamp, command, reply);
|
||||
} else if (memcmp(command, "del ", 4) == 0) {
|
||||
handleDelCmd(command, reply);
|
||||
} else if (sender_timestamp == 0 && strcmp(command, "erase") == 0) {
|
||||
bool s = _callbacks->formatFileSystem();
|
||||
sprintf(reply, "File system erase: %s", s ? "OK" : "Err");
|
||||
@@ -663,13 +975,21 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
#endif
|
||||
} else if (memcmp(command, "powersaving on", 14) == 0) {
|
||||
#if defined(NRF52_PLATFORM)
|
||||
_prefs->powersaving_enabled = 1;
|
||||
savePrefs();
|
||||
strcpy(reply, "on - Immediate effect");
|
||||
if (sender_timestamp == 0 || _board->isUsbDataConnected()) {
|
||||
strcpy(reply, "Error: USB serial connected");
|
||||
} else {
|
||||
_prefs->powersaving_enabled = 1;
|
||||
savePrefs();
|
||||
strcpy(reply, "on - Immediate effect");
|
||||
}
|
||||
#elif defined(ESP32) && !defined(WITH_BRIDGE)
|
||||
_prefs->powersaving_enabled = 1;
|
||||
savePrefs();
|
||||
strcpy(reply, "on - After 2 minutes");
|
||||
if (sender_timestamp == 0 || _board->isUsbDataConnected()) {
|
||||
strcpy(reply, "Error: USB serial connected");
|
||||
} else {
|
||||
_prefs->powersaving_enabled = 1;
|
||||
savePrefs();
|
||||
strcpy(reply, "on - After 2 minutes");
|
||||
}
|
||||
#elif defined(WITH_BRIDGE)
|
||||
strcpy(reply, "Bridge not supported");
|
||||
#else
|
||||
@@ -862,7 +1182,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f;
|
||||
uint8_t sf = num > 2 ? atoi(parts[2]) : 0;
|
||||
uint8_t cr = num > 3 ? atoi(parts[3]) : 0;
|
||||
if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) {
|
||||
if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && isValidLoRaBandwidth(bw)) {
|
||||
_prefs->sf = sf;
|
||||
_prefs->cr = cr;
|
||||
_prefs->freq = freq;
|
||||
@@ -872,6 +1192,28 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
} else {
|
||||
strcpy(reply, "Error, invalid radio params");
|
||||
}
|
||||
} else if (memcmp(config, "radioat ", 8) == 0) {
|
||||
float freq, bw;
|
||||
uint8_t sf, cr;
|
||||
uint32_t start_time, end_time;
|
||||
if (!parseScheduledRadioArgs(&config[8], false, freq, bw, sf, cr, start_time, end_time)) {
|
||||
strcpy(reply, "Error, use: set radioat f,bw,sf,cr,start");
|
||||
} else if (freq < 150.0f || freq > 2500.0f || sf < 5 || sf > 12 || cr < 5 || cr > 8 || !isValidLoRaBandwidth(bw)) {
|
||||
strcpy(reply, "Error, invalid radio params");
|
||||
} else {
|
||||
_callbacks->addScheduledRadioParams(false, freq, bw, sf, cr, start_time, end_time, reply);
|
||||
}
|
||||
} else if (memcmp(config, "tempradioat ", 12) == 0) {
|
||||
float freq, bw;
|
||||
uint8_t sf, cr;
|
||||
uint32_t start_time, end_time;
|
||||
if (!parseScheduledRadioArgs(&config[12], true, freq, bw, sf, cr, start_time, end_time)) {
|
||||
strcpy(reply, "Error, use: set tempradioat f,bw,sf,cr,start,end");
|
||||
} else if (freq < 150.0f || freq > 2500.0f || sf < 5 || sf > 12 || cr < 5 || cr > 8 || !isValidLoRaBandwidth(bw)) {
|
||||
strcpy(reply, "Error, invalid radio params");
|
||||
} else {
|
||||
_callbacks->addScheduledRadioParams(true, freq, bw, sf, cr, start_time, end_time, reply);
|
||||
}
|
||||
} else if (memcmp(config, "lat ", 4) == 0) {
|
||||
_prefs->node_lat = atof(&config[4]);
|
||||
savePrefs();
|
||||
@@ -955,6 +1297,18 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
} else {
|
||||
strcpy(reply, "Error, must be on or off");
|
||||
}
|
||||
} else if (memcmp(config, "direct.retry.heard ", 19) == 0) {
|
||||
if (strcmp(&config[19], "on") == 0) {
|
||||
_prefs->direct_retry_recent_enabled = 1;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else if (strcmp(&config[19], "off") == 0) {
|
||||
_prefs->direct_retry_recent_enabled = 0;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be on or off");
|
||||
}
|
||||
} else if (memcmp(config, "direct.retry.margin ", 20) == 0) {
|
||||
if (!looksNumeric(&config[20])) {
|
||||
strcpy(reply, "Error, must be 0-40 dB");
|
||||
@@ -999,6 +1353,81 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 0-5000 ms");
|
||||
}
|
||||
} else if (memcmp(config, "flood.retry.count ", 18) == 0) {
|
||||
int attempts = looksUnsignedInteger(&config[18]) ? _atoi(&config[18]) : -1;
|
||||
if (attempts >= 0 && attempts <= 15) {
|
||||
_prefs->flood_retry_attempts = (uint8_t)attempts;
|
||||
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 0-15");
|
||||
}
|
||||
} else if (memcmp(config, "flood.retry.path ", 17) == 0) {
|
||||
uint8_t path_gate;
|
||||
if (parseFloodRetryPathGate(&config[17], path_gate)) {
|
||||
_prefs->flood_retry_max_path = path_gate;
|
||||
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 0-63 or off");
|
||||
}
|
||||
} else if (memcmp(config, "flood.retry.prefixes ", 21) == 0) {
|
||||
if (parseFloodRetryPrefixList(_prefs->flood_retry_prefixes, FLOOD_RETRY_PREFIX_SLOTS, &config[21])) {
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes",
|
||||
(unsigned int)FLOOD_RETRY_PREFIX_SLOTS);
|
||||
}
|
||||
} else if (memcmp(config, "flood.retry.ignore ", 19) == 0) {
|
||||
if (parseFloodRetryPrefixList(_prefs->flood_retry_ignore_prefixes,
|
||||
FLOOD_RETRY_IGNORE_PREFIXES, &config[19])) {
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes",
|
||||
(unsigned int)FLOOD_RETRY_IGNORE_PREFIXES);
|
||||
}
|
||||
} else if (memcmp(config, "flood.retry.advert ", 19) == 0) {
|
||||
if (strcmp(&config[19], "on") == 0) {
|
||||
_prefs->flood_retry_advert_enabled = 1;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else if (strcmp(&config[19], "off") == 0) {
|
||||
_prefs->flood_retry_advert_enabled = 0;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be on or off");
|
||||
}
|
||||
} else if (memcmp(config, "flood.retry.bridge ", 19) == 0) {
|
||||
if (strcmp(&config[19], "on") == 0) {
|
||||
_prefs->flood_retry_bridge_enabled = 1;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else if (strcmp(&config[19], "off") == 0) {
|
||||
_prefs->flood_retry_bridge_enabled = 0;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be on or off");
|
||||
}
|
||||
} else if (memcmp(config, "flood.retry.bucket ", 19) == 0) {
|
||||
const char* params = &config[19];
|
||||
uint8_t bucket = atoi(params);
|
||||
const char* list = strchr(params, ' ');
|
||||
if (bucket < 1 || bucket > FLOOD_RETRY_BRIDGE_BUCKETS || list == NULL || *(list + 1) == 0) {
|
||||
sprintf(reply, "Error, usage: set flood.retry.bucket <1-%d> <prefixes|none>", FLOOD_RETRY_BRIDGE_BUCKETS);
|
||||
} else if (parseFloodRetryPrefixList(_prefs->flood_retry_bridge_buckets[bucket - 1],
|
||||
FLOOD_RETRY_BUCKET_PREFIXES, list + 1)) {
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes",
|
||||
(unsigned int)FLOOD_RETRY_BUCKET_PREFIXES);
|
||||
}
|
||||
} else if (memcmp(config, "direct.retry.cr ", 16) == 0) {
|
||||
if (strcmp(&config[16], "off") == 0) {
|
||||
_prefs->direct_retry_cr_enabled = 0;
|
||||
@@ -1234,6 +1663,10 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
} else {
|
||||
sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off");
|
||||
}
|
||||
} else if (memcmp(config, "tempradioat", 11) == 0 && (config[11] == 0 || config[11] == ' ')) {
|
||||
_callbacks->formatScheduledRadioParams(true, skipSpacesConst(&config[11]), reply);
|
||||
} else if (memcmp(config, "radioat", 7) == 0 && (config[7] == 0 || config[7] == ' ')) {
|
||||
_callbacks->formatScheduledRadioParams(false, skipSpacesConst(&config[7]), reply);
|
||||
} else if (memcmp(config, "radio", 5) == 0) {
|
||||
char freq[16], bw[16];
|
||||
strcpy(freq, StrHelper::ftoa(_prefs->freq));
|
||||
@@ -1255,6 +1688,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
sprintf(reply, "> %s", retryPresetName(_prefs->retry_preset));
|
||||
} else if (memcmp(config, "direct.retry", 12) == 0 && (config[12] == 0 || config[12] == ' ')) {
|
||||
sprintf(reply, "> %s", _prefs->direct_retry_enabled ? "on" : "off");
|
||||
} else if (memcmp(config, "direct.retry.heard", 18) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off");
|
||||
} else if (memcmp(config, "direct.retry.margin", 19) == 0) {
|
||||
char margin[12];
|
||||
formatSnrDbX4(margin, sizeof(margin), _prefs->direct_retry_snr_margin_x4);
|
||||
@@ -1265,6 +1700,30 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_base_ms);
|
||||
} else if (memcmp(config, "direct.retry.step", 17) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_step_ms);
|
||||
} else if (memcmp(config, "flood.retry.count", 17) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->flood_retry_attempts);
|
||||
} else if (memcmp(config, "flood.retry.path", 16) == 0) {
|
||||
char path_gate[8];
|
||||
formatFloodRetryPathGate(path_gate, _prefs->flood_retry_max_path);
|
||||
sprintf(reply, "> %s", path_gate);
|
||||
} else if (memcmp(config, "flood.retry.prefixes", 20) == 0) {
|
||||
formatFloodRetryPrefixList(tmp, _prefs->flood_retry_prefixes, FLOOD_RETRY_PREFIX_SLOTS);
|
||||
sprintf(reply, "> %s", tmp[0] ? tmp : "none");
|
||||
} else if (memcmp(config, "flood.retry.ignore", 18) == 0) {
|
||||
formatFloodRetryPrefixList(tmp, _prefs->flood_retry_ignore_prefixes, FLOOD_RETRY_IGNORE_PREFIXES);
|
||||
sprintf(reply, "> %s", tmp[0] ? tmp : "none");
|
||||
} else if (memcmp(config, "flood.retry.advert", 18) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->flood_retry_advert_enabled ? "on" : "off");
|
||||
} else if (memcmp(config, "flood.retry.bridge", 18) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->flood_retry_bridge_enabled ? "on" : "off");
|
||||
} else if (memcmp(config, "flood.retry.bucket.", 19) == 0) {
|
||||
uint8_t bucket = atoi(&config[19]);
|
||||
if (bucket >= 1 && bucket <= FLOOD_RETRY_BRIDGE_BUCKETS) {
|
||||
formatFloodRetryPrefixList(tmp, _prefs->flood_retry_bridge_buckets[bucket - 1], FLOOD_RETRY_BUCKET_PREFIXES);
|
||||
sprintf(reply, "> %s", tmp[0] ? tmp : "none");
|
||||
} else {
|
||||
sprintf(reply, "Error, bucket 1-%d", FLOOD_RETRY_BRIDGE_BUCKETS);
|
||||
}
|
||||
} else if (memcmp(config, "direct.retry.cr", 15) == 0) {
|
||||
if (!_prefs->direct_retry_cr_enabled) {
|
||||
strcpy(reply, "> off");
|
||||
@@ -1398,6 +1857,18 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
}
|
||||
}
|
||||
|
||||
void CommonCLI::handleDelCmd(char* command, char* reply) {
|
||||
const char* config = &command[4];
|
||||
if (memcmp(config, "tempradioat", 11) == 0 && (config[11] == 0 || config[11] == ' ')) {
|
||||
_callbacks->deleteScheduledRadioParams(true, skipSpacesConst(&config[11]), reply);
|
||||
} else if (memcmp(config, "radioat", 7) == 0 && (config[7] == 0 || config[7] == ' ')) {
|
||||
_callbacks->deleteScheduledRadioParams(false, skipSpacesConst(&config[7]), reply);
|
||||
} else {
|
||||
strcpy(reply, "unknown del: ");
|
||||
StrHelper::strncpy(&reply[13], config, 160 - 14);
|
||||
}
|
||||
}
|
||||
|
||||
static char* skipSpaces(char* s) {
|
||||
while (*s == ' ') s++;
|
||||
return s;
|
||||
|
||||
+73
-1
@@ -38,6 +38,45 @@
|
||||
#define DIRECT_RETRY_MOBILE_COUNT 15
|
||||
#define DIRECT_RETRY_MOBILE_STEP_MS 50
|
||||
#define DIRECT_RETRY_MOBILE_MARGIN_X4 0
|
||||
#define DIRECT_RETRY_RECENT_DEFAULT 1
|
||||
|
||||
#define FLOOD_RETRY_INFRA_COUNT 1
|
||||
#define FLOOD_RETRY_INFRA_MAX_PATH 1
|
||||
|
||||
#define FLOOD_RETRY_ROOFTOP_COUNT 3
|
||||
#define FLOOD_RETRY_ROOFTOP_MAX_PATH 2
|
||||
|
||||
#define FLOOD_RETRY_MOBILE_COUNT 15
|
||||
#define FLOOD_RETRY_MOBILE_MAX_PATH 1
|
||||
#define FLOOD_RETRY_ADVERT_DEFAULT 0
|
||||
|
||||
#define BATTERY_ALERT_LOW_PERCENT_DEFAULT 20
|
||||
#define BATTERY_ALERT_CRITICAL_PERCENT_DEFAULT 10
|
||||
|
||||
#ifndef FLOOD_RETRY_PREFIX_SLOTS
|
||||
#define FLOOD_RETRY_PREFIX_SLOTS 8
|
||||
#endif
|
||||
#ifndef FLOOD_RETRY_PREFIX_LEN
|
||||
#define FLOOD_RETRY_PREFIX_LEN 3
|
||||
#endif
|
||||
#ifndef FLOOD_RETRY_BRIDGE_BUCKETS
|
||||
#define FLOOD_RETRY_BRIDGE_BUCKETS 6
|
||||
#endif
|
||||
#ifndef FLOOD_RETRY_BUCKET_PREFIXES
|
||||
#define FLOOD_RETRY_BUCKET_PREFIXES 17
|
||||
#endif
|
||||
#ifndef FLOOD_RETRY_IGNORE_PREFIXES
|
||||
#define FLOOD_RETRY_IGNORE_PREFIXES 8
|
||||
#endif
|
||||
#ifndef FLOOD_RETRY_LIST_PREFIXES
|
||||
#define FLOOD_RETRY_LIST_PREFIXES ((FLOOD_RETRY_IGNORE_PREFIXES > FLOOD_RETRY_BUCKET_PREFIXES) ? FLOOD_RETRY_IGNORE_PREFIXES : FLOOD_RETRY_BUCKET_PREFIXES)
|
||||
#endif
|
||||
#ifndef FLOOD_RETRY_LIST_TEXT_MAX
|
||||
#define FLOOD_RETRY_LIST_TEXT_MAX (FLOOD_RETRY_LIST_PREFIXES * FLOOD_RETRY_PREFIX_LEN * 2 + FLOOD_RETRY_LIST_PREFIXES)
|
||||
#endif
|
||||
#ifndef COMMON_CLI_TMP_LEN
|
||||
#define COMMON_CLI_TMP_LEN ((FLOOD_RETRY_LIST_TEXT_MAX > (PRV_KEY_SIZE * 2 + 4)) ? FLOOD_RETRY_LIST_TEXT_MAX : (PRV_KEY_SIZE * 2 + 4))
|
||||
#endif
|
||||
|
||||
#define DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT 40
|
||||
#define DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT 30
|
||||
@@ -106,6 +145,17 @@ struct NodePrefs { // persisted to file
|
||||
uint8_t direct_retry_enabled;
|
||||
uint8_t direct_retry_cr_enabled;
|
||||
uint8_t direct_retry_prefs_magic[2];
|
||||
uint8_t flood_retry_attempts;
|
||||
uint8_t flood_retry_max_path;
|
||||
uint8_t flood_retry_prefixes[FLOOD_RETRY_PREFIX_SLOTS][FLOOD_RETRY_PREFIX_LEN];
|
||||
uint8_t flood_retry_bridge_enabled;
|
||||
uint8_t flood_retry_bridge_buckets[FLOOD_RETRY_BRIDGE_BUCKETS][FLOOD_RETRY_BUCKET_PREFIXES][FLOOD_RETRY_PREFIX_LEN];
|
||||
uint8_t flood_retry_ignore_prefixes[FLOOD_RETRY_IGNORE_PREFIXES][FLOOD_RETRY_PREFIX_LEN];
|
||||
uint8_t flood_retry_advert_enabled;
|
||||
uint8_t battery_alert_enabled;
|
||||
uint8_t battery_alert_low_percent;
|
||||
uint8_t battery_alert_critical_percent;
|
||||
uint8_t direct_retry_recent_enabled;
|
||||
};
|
||||
|
||||
class CommonCLICallbacks {
|
||||
@@ -145,6 +195,27 @@ public:
|
||||
virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0;
|
||||
virtual void clearStats() = 0;
|
||||
virtual void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) = 0;
|
||||
virtual void addScheduledRadioParams(bool temporary, float freq, float bw, uint8_t sf, uint8_t cr,
|
||||
uint32_t start_time, uint32_t end_time, char* reply) {
|
||||
(void)temporary;
|
||||
(void)freq;
|
||||
(void)bw;
|
||||
(void)sf;
|
||||
(void)cr;
|
||||
(void)start_time;
|
||||
(void)end_time;
|
||||
strcpy(reply, "Error: unsupported");
|
||||
}
|
||||
virtual void formatScheduledRadioParams(bool temporary, const char* selector, char* reply) {
|
||||
(void)temporary;
|
||||
(void)selector;
|
||||
strcpy(reply, "Error: unsupported");
|
||||
}
|
||||
virtual void deleteScheduledRadioParams(bool temporary, const char* selector, char* reply) {
|
||||
(void)temporary;
|
||||
(void)selector;
|
||||
strcpy(reply, "Error: unsupported");
|
||||
}
|
||||
|
||||
virtual void startRegionsLoad() {
|
||||
// no op by default
|
||||
@@ -177,7 +248,7 @@ class CommonCLI {
|
||||
SensorManager* _sensors;
|
||||
RegionMap* _region_map;
|
||||
ClientACL* _acl;
|
||||
char tmp[PRV_KEY_SIZE*2 + 4];
|
||||
char tmp[COMMON_CLI_TMP_LEN];
|
||||
|
||||
mesh::RTCClock* getRTCClock() { return _rtc; }
|
||||
void savePrefs();
|
||||
@@ -186,6 +257,7 @@ class CommonCLI {
|
||||
void handleRegionCmd(char* command, char* reply);
|
||||
void handleGetCmd(uint32_t sender_timestamp, char* command, char* reply);
|
||||
void handleSetCmd(uint32_t sender_timestamp, char* command, char* reply);
|
||||
void handleDelCmd(char* command, char* reply);
|
||||
|
||||
public:
|
||||
CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, RegionMap& region_map, ClientACL& acl, NodePrefs* prefs, CommonCLICallbacks* callbacks)
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
|
||||
bool ESP32Board::startOTAUpdate(const char* id, char reply[]) {
|
||||
inhibit_sleep = true; // prevent sleep during OTA
|
||||
|
||||
if (ota_server != nullptr) {
|
||||
sprintf(reply, "Started: http://%s/update", WiFi.softAPIP().toString().c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
WiFi.softAP("MeshCore-OTA", NULL);
|
||||
|
||||
sprintf(reply, "Started: http://%s/update", WiFi.softAPIP().toString().c_str());
|
||||
@@ -23,18 +29,36 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[]) {
|
||||
static char home_buf[90];
|
||||
sprintf(home_buf, "<H2>Hi! I am a MeshCore Repeater. ID: %s</H2>", id);
|
||||
|
||||
AsyncWebServer* server = new AsyncWebServer(80);
|
||||
ota_server = new AsyncWebServer(80);
|
||||
|
||||
server->on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
ota_server->on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/html", home_buf);
|
||||
});
|
||||
server->on("/log", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
ota_server->on("/log", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(SPIFFS, "/packet_log", "text/plain");
|
||||
});
|
||||
|
||||
AsyncElegantOTA.setID(id_buf);
|
||||
AsyncElegantOTA.begin(server); // Start ElegantOTA
|
||||
server->begin();
|
||||
AsyncElegantOTA.begin(ota_server); // Start ElegantOTA
|
||||
ota_server->begin();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ESP32Board::stopOTAUpdate(char reply[]) {
|
||||
if (ota_server == nullptr) {
|
||||
strcpy(reply, "OK - OTA not running");
|
||||
return true;
|
||||
}
|
||||
|
||||
ota_server->end();
|
||||
delete ota_server;
|
||||
ota_server = nullptr;
|
||||
WiFi.softAPdisconnect(true);
|
||||
inhibit_sleep = false;
|
||||
|
||||
strcpy(reply, "OK - OTA stopped");
|
||||
MESH_DEBUG_PRINTLN("stopOTAUpdate: %s", reply);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -43,6 +67,10 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[]) {
|
||||
bool ESP32Board::startOTAUpdate(const char* id, char reply[]) {
|
||||
return false; // not supported
|
||||
}
|
||||
|
||||
bool ESP32Board::stopOTAUpdate(char reply[]) {
|
||||
return false; // not supported
|
||||
}
|
||||
#endif
|
||||
|
||||
void ESP32Board::powerOff() {
|
||||
|
||||
@@ -16,10 +16,13 @@
|
||||
#include "esp_system.h"
|
||||
#include <driver/rtc_io.h>
|
||||
|
||||
class AsyncWebServer;
|
||||
|
||||
class ESP32Board : public mesh::MainBoard {
|
||||
protected:
|
||||
uint8_t startup_reason;
|
||||
bool inhibit_sleep = false;
|
||||
AsyncWebServer* ota_server = nullptr;
|
||||
static inline portMUX_TYPE sleepMux = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
public:
|
||||
@@ -155,6 +158,15 @@ public:
|
||||
}
|
||||
|
||||
bool startOTAUpdate(const char* id, char reply[]) override;
|
||||
bool stopOTAUpdate(char reply[]) override;
|
||||
|
||||
bool isUsbDataConnected() override {
|
||||
#if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
|
||||
return (bool)Serial;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void setInhibitSleep(bool inhibit) {
|
||||
inhibit_sleep = inhibit;
|
||||
|
||||
+67
-15
@@ -3,18 +3,33 @@
|
||||
#include <target.h>
|
||||
|
||||
#include <bluefruit.h>
|
||||
#include "ble_gap.h"
|
||||
#include "ble_hci.h"
|
||||
#include <nrf_soc.h>
|
||||
|
||||
static BLEDfu bledfu;
|
||||
static uint16_t ota_conn_handle = BLE_CONN_HANDLE_INVALID;
|
||||
static bool ota_active = false;
|
||||
static bool ota_ble_started = false;
|
||||
|
||||
static void format_ota_reply(char reply[]) {
|
||||
uint8_t mac_addr[6];
|
||||
memset(mac_addr, 0, sizeof(mac_addr));
|
||||
Bluefruit.getAddr(mac_addr);
|
||||
sprintf(reply, "OK - mac: %02X:%02X:%02X:%02X:%02X:%02X", mac_addr[5], mac_addr[4], mac_addr[3],
|
||||
mac_addr[2], mac_addr[1], mac_addr[0]);
|
||||
}
|
||||
|
||||
static void connect_callback(uint16_t conn_handle) {
|
||||
(void)conn_handle;
|
||||
ota_conn_handle = conn_handle;
|
||||
MESH_DEBUG_PRINTLN("BLE client connected");
|
||||
}
|
||||
|
||||
static void disconnect_callback(uint16_t conn_handle, uint8_t reason) {
|
||||
(void)conn_handle;
|
||||
(void)reason;
|
||||
if (ota_conn_handle == conn_handle) {
|
||||
ota_conn_handle = BLE_CONN_HANDLE_INVALID;
|
||||
}
|
||||
|
||||
MESH_DEBUG_PRINTLN("BLE client disconnected");
|
||||
}
|
||||
@@ -252,6 +267,14 @@ bool NRF52Board::isExternalPowered() {
|
||||
}
|
||||
}
|
||||
|
||||
bool NRF52Board::isUsbDataConnected() {
|
||||
#ifdef USE_TINYUSB
|
||||
return Serial.dtr();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void NRF52Board::sleep(uint32_t secs) {
|
||||
// Clear FPU interrupt flags to avoid insomnia
|
||||
// see errata 87 for details https://docs.nordicsemi.com/bundle/errata_nRF52840_Rev3/page/ERR/nRF52840/Rev3/latest/anomaly_840_87.html
|
||||
@@ -349,13 +372,27 @@ bool NRF52Board::getBootloaderVersion(char* out, size_t max_len) {
|
||||
}
|
||||
|
||||
bool NRF52Board::startOTAUpdate(const char *id, char reply[]) {
|
||||
// Config the peripheral connection with maximum bandwidth
|
||||
// more SRAM required by SoftDevice
|
||||
// Note: All config***() function must be called before begin()
|
||||
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
|
||||
Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16);
|
||||
(void)id;
|
||||
|
||||
if (ota_active) {
|
||||
format_ota_reply(reply);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ota_ble_started) {
|
||||
// Config the peripheral connection with maximum bandwidth
|
||||
// more SRAM required by SoftDevice
|
||||
// Note: All config***() function must be called before begin()
|
||||
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
|
||||
Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16);
|
||||
|
||||
Bluefruit.begin(1, 0);
|
||||
ota_ble_started = true;
|
||||
|
||||
// To be consistent OTA DFU should be added first if it exists
|
||||
bledfu.begin();
|
||||
}
|
||||
|
||||
Bluefruit.begin(1, 0);
|
||||
// Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4
|
||||
Bluefruit.setTxPower(4);
|
||||
// Set the BLE device name
|
||||
@@ -364,8 +401,8 @@ bool NRF52Board::startOTAUpdate(const char *id, char reply[]) {
|
||||
Bluefruit.Periph.setConnectCallback(connect_callback);
|
||||
Bluefruit.Periph.setDisconnectCallback(disconnect_callback);
|
||||
|
||||
// To be consistent OTA DFU should be added first if it exists
|
||||
bledfu.begin();
|
||||
Bluefruit.Advertising.clearData();
|
||||
Bluefruit.ScanResponse.clearData();
|
||||
|
||||
// Set up and start advertising
|
||||
// Advertising packet
|
||||
@@ -387,12 +424,27 @@ bool NRF52Board::startOTAUpdate(const char *id, char reply[]) {
|
||||
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
|
||||
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
|
||||
|
||||
uint8_t mac_addr[6];
|
||||
memset(mac_addr, 0, sizeof(mac_addr));
|
||||
Bluefruit.getAddr(mac_addr);
|
||||
sprintf(reply, "OK - mac: %02X:%02X:%02X:%02X:%02X:%02X", mac_addr[5], mac_addr[4], mac_addr[3],
|
||||
mac_addr[2], mac_addr[1], mac_addr[0]);
|
||||
ota_active = true;
|
||||
format_ota_reply(reply);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NRF52Board::stopOTAUpdate(char reply[]) {
|
||||
if (!ota_active) {
|
||||
strcpy(reply, "OK - OTA not running");
|
||||
return true;
|
||||
}
|
||||
|
||||
Bluefruit.Advertising.restartOnDisconnect(false);
|
||||
Bluefruit.Advertising.stop();
|
||||
if (ota_conn_handle != BLE_CONN_HANDLE_INVALID) {
|
||||
sd_ble_gap_disconnect(ota_conn_handle, BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
|
||||
ota_conn_handle = BLE_CONN_HANDLE_INVALID;
|
||||
}
|
||||
ota_active = false;
|
||||
|
||||
strcpy(reply, "OK - OTA stopped");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -53,8 +53,10 @@ public:
|
||||
virtual void powerOff() override;
|
||||
virtual bool getBootloaderVersion(char* version, size_t max_len) override;
|
||||
virtual bool startOTAUpdate(const char *id, char reply[]) override;
|
||||
virtual bool stopOTAUpdate(char reply[]) override;
|
||||
virtual void sleep(uint32_t secs) override;
|
||||
bool isExternalPowered() override;
|
||||
bool isUsbDataConnected() override;
|
||||
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
uint16_t getBootVoltage() override { return boot_voltage_mv; }
|
||||
@@ -77,4 +79,4 @@ public:
|
||||
NRF52BoardDCDC() {}
|
||||
virtual void begin() override;
|
||||
};
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <Mesh.h>
|
||||
#if ARDUINO
|
||||
#include <Arduino.h>
|
||||
#endif
|
||||
|
||||
#ifdef ESP32
|
||||
#include <FS.h>
|
||||
@@ -26,6 +29,7 @@ public:
|
||||
uint8_t prefix[MAX_ROUTE_HASH_BYTES];
|
||||
uint8_t prefix_len;
|
||||
int8_t snr_x4;
|
||||
uint32_t last_heard_millis;
|
||||
};
|
||||
|
||||
private:
|
||||
@@ -194,7 +198,10 @@ public:
|
||||
uint32_t getNumDirectDups() const { return _direct_dups; }
|
||||
uint32_t getNumFloodDups() const { return _flood_dups; }
|
||||
|
||||
bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) {
|
||||
bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4,
|
||||
bool snr_locked = false, bool bypass_allow_filter = false) {
|
||||
(void)snr_locked;
|
||||
(void)bypass_allow_filter;
|
||||
if (prefix == NULL || prefix_len == 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -211,6 +218,11 @@ public:
|
||||
continue;
|
||||
}
|
||||
existing.snr_x4 = weightedSnrX4RoundUp(existing.snr_x4, snr_x4);
|
||||
#if ARDUINO
|
||||
existing.last_heard_millis = millis();
|
||||
#else
|
||||
existing.last_heard_millis = 0;
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -222,15 +234,19 @@ public:
|
||||
}
|
||||
}
|
||||
if (slot_idx < 0) {
|
||||
// Table is full: evict the weakest observed SNR entry.
|
||||
// Table is full: evict the oldest heard entry.
|
||||
slot_idx = 0;
|
||||
int8_t min_snr_x4 = _recent_repeaters[0].snr_x4;
|
||||
#if ARDUINO
|
||||
uint32_t now = millis();
|
||||
uint32_t oldest_age = (uint32_t)(now - _recent_repeaters[0].last_heard_millis);
|
||||
for (int i = 1; i < MAX_RECENT_REPEATERS; i++) {
|
||||
if (_recent_repeaters[i].snr_x4 < min_snr_x4) {
|
||||
min_snr_x4 = _recent_repeaters[i].snr_x4;
|
||||
uint32_t age = (uint32_t)(now - _recent_repeaters[i].last_heard_millis);
|
||||
if (age > oldest_age) {
|
||||
oldest_age = age;
|
||||
slot_idx = i;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
RecentRepeaterInfo& slot = _recent_repeaters[slot_idx];
|
||||
@@ -238,6 +254,11 @@ public:
|
||||
memcpy(slot.prefix, prefix, prefix_len);
|
||||
slot.prefix_len = prefix_len;
|
||||
slot.snr_x4 = snr_x4;
|
||||
#if ARDUINO
|
||||
slot.last_heard_millis = millis();
|
||||
#else
|
||||
slot.last_heard_millis = 0;
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
bool decrementRecentRepeaterSnrX4(const uint8_t* prefix, uint8_t prefix_len, uint8_t amount_x4 = 1) {
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
#include "RadioLibWrappers.h"
|
||||
#include "LR11x0Reset.h"
|
||||
|
||||
#ifndef USE_LR1110
|
||||
#define USE_LR1110
|
||||
#endif
|
||||
|
||||
class CustomLR1110Wrapper : public RadioLibWrapper {
|
||||
public:
|
||||
CustomLR1110Wrapper(CustomLR1110& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
|
||||
@@ -12,6 +12,7 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-D PIN_STATUS_LED=39
|
||||
-D P_LORA_TX_LED=22
|
||||
-D RADIO_CLASS=CustomLR1110
|
||||
-D USE_LR1110
|
||||
-D WRAPPER_CLASS=CustomLR1110Wrapper
|
||||
-D LORA_TX_POWER=22
|
||||
-D ENV_INCLUDE_GPS=0
|
||||
|
||||
@@ -12,6 +12,7 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-D PIN_USER_BTN=12
|
||||
-D PIN_STATUS_LED=35
|
||||
-D RADIO_CLASS=CustomLR1110
|
||||
-D USE_LR1110
|
||||
-D WRAPPER_CLASS=CustomLR1110Wrapper
|
||||
-D LORA_TX_POWER=22
|
||||
-D RF_SWITCH_TABLE
|
||||
|
||||
@@ -11,6 +11,7 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-D WIO_WM1110
|
||||
; -D MESH_DEBUG=1
|
||||
-D RADIO_CLASS=CustomLR1110
|
||||
-D USE_LR1110
|
||||
-D WRAPPER_CLASS=CustomLR1110Wrapper
|
||||
-D LORA_TX_POWER=22
|
||||
-D RX_BOOSTED_GAIN=true
|
||||
|
||||
Reference in New Issue
Block a user