Tune keymind repeater behavior

This commit is contained in:
mikecarper
2026-06-24 17:06:49 -07:00
parent 7473719b92
commit 48ac725d76
24 changed files with 2885 additions and 155 deletions
+3
View File
@@ -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
+66 -9
View File
@@ -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
View File
@@ -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.
+11 -15
View File
@@ -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:
File diff suppressed because it is too large Load Diff
+86 -6
View File
@@ -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 {
+7 -4
View File
@@ -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
}
+39 -1
View File
@@ -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);
}
+5
View File
@@ -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;
};
+7 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+33 -5
View File
@@ -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() {
+12
View File
@@ -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
View File
@@ -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
+3 -1
View File
@@ -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
+26 -5
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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