Improve repeater reliability and build tooling

This commit is contained in:
mikecarper
2026-07-13 01:03:27 -07:00
parent 2b4aed4ef1
commit da00a00436
31 changed files with 1079 additions and 229 deletions
+3
View File
@@ -5,6 +5,9 @@
.vscode/launch.json
.vscode/ipch
out/
# Local firmware archives and build logs
out.*/
build-logs/
.direnv/
.DS_Store
.vscode/settings.json
+298 -36
View File
@@ -24,6 +24,12 @@ FIRMWARE_PROFILE_OVERRIDE="${FIRMWARE_PROFILE_OVERRIDE:-}"
BATCH_BUILD_MODE=0
RESOLVED_BUILD_TARGETS=()
RESUME_BUILD_OUTPUT="${RESUME_BUILD_OUTPUT:-0}"
LOGGING_MATRIX_FAILURES=()
RADIO_PRESET_SELECTION=""
KISS_MODE_OVERRIDE=""
PARSED_COMMAND_ARGS=()
FIRMWARE_VERSION_EXPLICIT=0
OUTPUT_POLICY_EXPLICIT=0
ENV_VARIANT_SUFFIX_PATTERN='companion_radio_serial|companion_radio_wifi|companion_radio_usb|comp_radio_usb|companion_usb|companion_radio_ble|companion_ble|repeater_bridge_rs232_serial1|repeater_bridge_rs232_serial2|repeater_bridge_rs232|repeater_bridge_espnow|terminal_chat|room_server|room_svr|kiss_modem|sensor|repeatr|repeater'
BOARD_MODIFIER_WITHOUT_DISPLAY="_without_display"
@@ -52,20 +58,27 @@ FALLBACK_VERSION_DATE_FORMAT='+%Y-%m-%d-%H-%M'
global_usage() {
cat - <<EOF
Usage:
bash build.sh <command> [target]
bash build.sh <command> [target] [options]
Commands:
help|usage|-h|--help: Shows this message.
list|-l: List firmwares available to build.
build-firmware <target>: Build the firmware for the given build target.
build-firmwares: Build all firmwares for all targets.
build-firmwares-logging-matrix: Build all firmwares in three profiles: logging off, logging on for non-Bluetooth targets, and MQTT bridge on with logging off.
build-firmwares-logging-matrix: Build all firmwares in three profiles, logging each target under out/build-logs/ and continuing after failures.
build-matching-firmwares <build-match-spec>: Build all firmwares for build targets containing the string given for <build-match-spec>.
build-companion-firmwares: Build all companion firmwares for all build targets.
build-repeater-firmwares: Build all repeater firmwares for all build targets.
build-room-server-firmwares: Build all chat room server firmwares for all build targets.
build-sensor-firmwares: Build all sensor firmwares for all build targets.
Options:
--firmware-version <version>: Firmware version to embed.
--radio-preset <number>: Use the numbered radio choice from the interactive menu (1 keeps target defaults).
--profile <default|cascade>: Select the firmware settings profile.
--skip-kiss|--include-kiss: Exclude or include KISS modem targets in bulk builds.
--clean|--resume: Clean output or resume existing option 3 artifacts.
Examples:
Build firmware for the "RAK_4631_repeater" device target
$ bash build.sh build-firmware RAK_4631_repeater
@@ -369,6 +382,103 @@ clear_firmware_profile_overrides() {
FIRMWARE_PROFILE_OVERRIDE=""
}
apply_cli_radio_preset() {
local selection=$1
local preset_output row
local -a preset_rows=()
if ! [[ "$selection" =~ ^[0-9]+$ ]] || [ "$selection" -lt 1 ]; then
echo "Invalid --radio-preset value: ${selection}"
return 1
fi
clear_radio_overrides
if [ "$selection" -eq 1 ]; then
echo "Using target default radio settings."
return 0
fi
if ! preset_output=$(fetch_suggested_radio_settings) || [ -z "$preset_output" ]; then
echo "Could not fetch radio preset ${selection} from ${RADIO_SETTINGS_API_URL}."
return 1
fi
mapfile -t preset_rows <<< "$preset_output"
if [ "$selection" -gt $((${#preset_rows[@]} + 1)) ]; then
echo "Radio preset ${selection} is out of range; available menu choices are 1-$((${#preset_rows[@]} + 1))."
return 1
fi
row=${preset_rows[$((selection - 2))]}
IFS=$'\t' read -r title description freq bw sf cr <<< "$row"
set_radio_overrides "$title" "$freq" "$bw" "$sf" "$cr"
echo "Using radio setting ${selection}: ${RADIO_SETTING_TITLE} (${RADIO_FREQ_OVERRIDE}MHz / SF${RADIO_SF_OVERRIDE} / BW${RADIO_BW_OVERRIDE} / CR${RADIO_CR_OVERRIDE})"
}
parse_cli_options() {
local -a positional=()
while [ $# -gt 0 ]; do
case "$1" in
--firmware-version|--version)
if [ $# -lt 2 ] || [ -z "$2" ]; then echo "$1 requires a value"; return 1; fi
FIRMWARE_VERSION=$2
FIRMWARE_VERSION_EXPLICIT=1
export FIRMWARE_VERSION
shift 2
;;
--radio-preset)
if [ $# -lt 2 ] || [ -z "$2" ]; then echo "$1 requires a value"; return 1; fi
RADIO_PRESET_SELECTION=$2
shift 2
;;
--profile)
if [ $# -lt 2 ]; then echo "$1 requires a value"; return 1; fi
case "${2,,}" in
default) FIRMWARE_PROFILE_OVERRIDE="" ;;
cascade) FIRMWARE_PROFILE_OVERRIDE="cascade" ;;
*) echo "Invalid profile: $2 (use default or cascade)"; return 1 ;;
esac
shift 2
;;
--skip-kiss)
KISS_MODE_OVERRIDE="skip"
shift
;;
--include-kiss)
KISS_MODE_OVERRIDE="build"
shift
;;
--clean)
RESUME_BUILD_OUTPUT=0
OUTPUT_POLICY_EXPLICIT=1
shift
;;
--resume)
RESUME_BUILD_OUTPUT=1
OUTPUT_POLICY_EXPLICIT=1
shift
;;
--)
shift
positional+=("$@")
break
;;
help|usage|-h|--help|list|-l)
positional+=("$1")
shift
;;
-*)
echo "Unknown option: $1"
return 1
;;
*)
positional+=("$1")
shift
;;
esac
done
PARSED_COMMAND_ARGS=("${positional[@]}")
}
set_radio_overrides() {
RADIO_SETTING_TITLE=$1
RADIO_FREQ_OVERRIDE=$2
@@ -920,6 +1030,11 @@ prompt_for_resolved_firmware_version() {
return 0
fi
if [ "$FIRMWARE_VERSION_EXPLICIT" -eq 1 ]; then
echo "Using firmware version from --firmware-version: ${FIRMWARE_VERSION}"
return 0
fi
if ! [ -t 0 ]; then
return 0
fi
@@ -1036,6 +1151,27 @@ filter_out_bluetooth_targets() {
done
}
is_logging_size_constrained_target() {
case "$1" in
Tiny_Relay_repeater|RAK_3x72_repeater|wio-e5_repeater)
return 0
;;
*)
return 1
;;
esac
}
filter_out_logging_size_constrained_targets() {
local target
for target in "$@"; do
if ! is_logging_size_constrained_target "$target"; then
printf '%s\n' "$target"
fi
done
}
prompt_for_kiss_modem_build_policy() {
local kiss_count=0
local target
@@ -1051,6 +1187,18 @@ prompt_for_kiss_modem_build_policy() {
return 0
fi
case "$KISS_MODE_OVERRIDE" in
skip)
filter_out_kiss_modem_targets
echo "Skipped ${kiss_count} KISS modem target(s)."
return 0
;;
build)
echo "Including ${kiss_count} KISS modem target(s)."
return 0
;;
esac
if ! [ -t 0 ]; then
echo "Including ${kiss_count} KISS modem target(s)."
return 0
@@ -1096,6 +1244,15 @@ prompt_for_logging_matrix_output_policy() {
normalize_resume_build_output
if [ "$OUTPUT_POLICY_EXPLICIT" -eq 1 ]; then
if [ "$RESUME_BUILD_OUTPUT" == "1" ]; then
echo "Using --resume for existing option 3 output."
else
echo "Using --clean for option 3 output."
fi
return 0
fi
if ! [ -d "$OUTPUT_DIR" ]; then
RESUME_BUILD_OUTPUT=0
return 0
@@ -1802,11 +1959,70 @@ run_resolved_build_targets() {
return "$build_status"
}
run_logged_build_targets() {
local targets=("$@")
local env profile log_dir log_path log_tmp
local previous_batch_build_mode=$BATCH_BUILD_MODE
local build_status=0
local overall_status=0
local preserved_log=0
if [ ${#targets[@]} -eq 0 ]; then
echo "No build targets resolved."
return 1
fi
if [ -n "$FIRMWARE_FILENAME_INFIX" ]; then
profile=$FIRMWARE_FILENAME_INFIX
elif [ "${MQTT_BRIDGE_OVERRIDE,,}" == "on" ]; then
profile="mqtt"
else
profile="standard"
fi
log_dir="${OUTPUT_DIR}/build-logs"
mkdir -p -- "$log_dir" || return 1
if [ ${#targets[@]} -gt 1 ]; then
BATCH_BUILD_MODE=1
fi
for env in "${targets[@]}"; do
log_path="${log_dir}/${env}-${profile}.log"
log_tmp="${log_path}.tmp"
preserved_log=0
echo "Building ${env} (${profile}); log: ${log_path}"
build_firmware "$env" > "$log_tmp" 2>&1
build_status=$?
if [ "$build_status" -eq 0 ] \
&& grep -q "^Skipping ${env}; existing artifacts found" "$log_tmp" \
&& [ -s "$log_path" ]; then
rm -f -- "$log_tmp"
preserved_log=1
else
mv -f -- "$log_tmp" "$log_path"
fi
if [ "$build_status" -ne 0 ]; then
overall_status=1
LOGGING_MATRIX_FAILURES+=("${env} (${profile}) -> ${log_path}")
echo "FAILED: ${env} (${profile}), status ${build_status}"
echo "FAILED: ${env} (${profile}), status ${build_status}" >> "$log_path"
else
echo "SUCCEEDED: ${env} (${profile})"
if [ "$preserved_log" -eq 0 ]; then
echo "SUCCEEDED: ${env} (${profile})" >> "$log_path"
fi
fi
done
BATCH_BUILD_MODE=$previous_batch_build_mode
return "$overall_status"
}
run_logging_matrix_build_targets() {
local targets=("$@")
local target
local standard_targets=()
local logging_targets=()
local constrained_logging_targets=()
local mqtt_targets=()
local original_meshdebug_override=$MESHDEBUG_OVERRIDE
local original_packet_logging_override=$PACKET_LOGGING_OVERRIDE
@@ -1814,12 +2030,15 @@ run_logging_matrix_build_targets() {
local original_mqtt_debug_override=$MQTT_DEBUG_OVERRIDE
local original_firmware_filename_infix=$FIRMWARE_FILENAME_INFIX
local bluetooth_skip_count=0
local logging_target_count=0
local build_status=0
local pass_status=0
if [ ${#targets[@]} -eq 0 ]; then
echo "No build targets resolved."
return 1
fi
LOGGING_MATRIX_FAILURES=()
for target in "${targets[@]}"; do
if is_mqtt_bridge_target "$target"; then
@@ -1835,45 +2054,66 @@ run_logging_matrix_build_targets() {
MQTT_BRIDGE_OVERRIDE="off"
FIRMWARE_FILENAME_INFIX=""
if [ ${#standard_targets[@]} -gt 0 ]; then
run_resolved_build_targets "${standard_targets[@]}"
build_status=$?
run_logged_build_targets "${standard_targets[@]}"
pass_status=$?
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
fi
if [ "$build_status" -eq 0 ]; then
mapfile -t logging_targets < <(filter_out_bluetooth_targets "${standard_targets[@]}")
bluetooth_skip_count=$((${#standard_targets[@]} - ${#logging_targets[@]}))
mapfile -t logging_targets < <(filter_out_bluetooth_targets "${standard_targets[@]}")
bluetooth_skip_count=$((${#standard_targets[@]} - ${#logging_targets[@]}))
if [ "$bluetooth_skip_count" -gt 0 ]; then
echo "Skipping ${bluetooth_skip_count} Bluetooth target(s) for logging-on pass."
fi
if [ ${#logging_targets[@]} -gt 0 ]; then
echo "Profile 2/3: building ${#logging_targets[@]} standard target(s) with logging on and MQTT bridge off."
echo "Logging-on artifacts use filename form: name-logging-version"
MESHDEBUG_OVERRIDE="on"
PACKET_LOGGING_OVERRIDE="on"
MQTT_BRIDGE_OVERRIDE="off"
FIRMWARE_FILENAME_INFIX="logging"
run_resolved_build_targets "${logging_targets[@]}"
build_status=$?
else
echo "No non-Bluetooth targets remain for logging-on pass."
fi
if [ "$bluetooth_skip_count" -gt 0 ]; then
echo "Skipping ${bluetooth_skip_count} Bluetooth target(s) for logging-on pass."
fi
if [ "$build_status" -eq 0 ]; then
if [ ${#mqtt_targets[@]} -gt 0 ]; then
echo "Profile 3/3: building ${#mqtt_targets[@]} MQTT bridge target(s) for direct radio-to-MQTT forwarding over WiFi, with logging off."
MESHDEBUG_OVERRIDE="off"
PACKET_LOGGING_OVERRIDE="off"
MQTT_BRIDGE_OVERRIDE="on"
MQTT_DEBUG_OVERRIDE="off"
FIRMWARE_FILENAME_INFIX=""
run_resolved_build_targets "${mqtt_targets[@]}"
build_status=$?
else
echo "No MQTT bridge targets are configured; skipping profile 3/3."
for target in "${logging_targets[@]}"; do
if is_logging_size_constrained_target "$target"; then
constrained_logging_targets+=("$target")
fi
done
mapfile -t logging_targets < <(filter_out_logging_size_constrained_targets "${logging_targets[@]}")
logging_target_count=$((${#logging_targets[@]} + ${#constrained_logging_targets[@]}))
if [ "$logging_target_count" -gt 0 ]; then
echo "Profile 2/3: building ${logging_target_count} standard target(s) with logging on and MQTT bridge off."
echo "Logging-on artifacts use filename form: name-logging-version"
else
echo "No non-Bluetooth targets remain for logging-on pass."
fi
if [ ${#logging_targets[@]} -gt 0 ]; then
MESHDEBUG_OVERRIDE="on"
PACKET_LOGGING_OVERRIDE="on"
MQTT_BRIDGE_OVERRIDE="off"
FIRMWARE_FILENAME_INFIX="logging"
run_logged_build_targets "${logging_targets[@]}"
pass_status=$?
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
fi
if [ ${#constrained_logging_targets[@]} -gt 0 ]; then
echo "Building ${#constrained_logging_targets[@]} 224KB STM32 repeater target(s) with packet logging on and MESH_DEBUG off to fit flash: Tiny_Relay_repeater, RAK_3x72_repeater, wio-e5_repeater."
MESHDEBUG_OVERRIDE="off"
PACKET_LOGGING_OVERRIDE="on"
MQTT_BRIDGE_OVERRIDE="off"
FIRMWARE_FILENAME_INFIX="logging"
run_logged_build_targets "${constrained_logging_targets[@]}"
pass_status=$?
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
fi
if [ ${#mqtt_targets[@]} -gt 0 ]; then
echo "Profile 3/3: building ${#mqtt_targets[@]} MQTT bridge target(s) for direct radio-to-MQTT forwarding over WiFi, with logging off."
MESHDEBUG_OVERRIDE="off"
PACKET_LOGGING_OVERRIDE="off"
MQTT_BRIDGE_OVERRIDE="on"
MQTT_DEBUG_OVERRIDE="off"
FIRMWARE_FILENAME_INFIX=""
run_logged_build_targets "${mqtt_targets[@]}"
pass_status=$?
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
else
echo "No MQTT bridge targets are configured; skipping profile 3/3."
fi
MESHDEBUG_OVERRIDE=$original_meshdebug_override
@@ -1882,6 +2122,15 @@ run_logging_matrix_build_targets() {
MQTT_DEBUG_OVERRIDE=$original_mqtt_debug_override
FIRMWARE_FILENAME_INFIX=$original_firmware_filename_infix
if [ ${#LOGGING_MATRIX_FAILURES[@]} -gt 0 ]; then
echo "Logging matrix completed with ${#LOGGING_MATRIX_FAILURES[@]} failed build(s):"
printf ' %s\n' "${LOGGING_MATRIX_FAILURES[@]}"
elif [ "$build_status" -ne 0 ]; then
echo "Logging matrix completed with an output/logging error; inspect ${OUTPUT_DIR}/build-logs/."
else
echo "Logging matrix completed successfully. Per-target logs are in ${OUTPUT_DIR}/build-logs/."
fi
return "$build_status"
}
@@ -1927,6 +2176,11 @@ run_command() {
}
main() {
if ! parse_cli_options "$@"; then
exit 1
fi
set -- "${PARSED_COMMAND_ARGS[@]}"
case "${1:-}" in
help|usage|-h|--help)
global_usage
@@ -1945,6 +2199,12 @@ main() {
init_project_context
if [ -n "$RADIO_PRESET_SELECTION" ]; then
if ! apply_cli_radio_preset "$RADIO_PRESET_SELECTION"; then
exit 1
fi
fi
if [ $# -eq 0 ]; then
if ! [ -t 0 ]; then
echo "No command provided and no interactive terminal is available."
@@ -1985,4 +2245,6 @@ main() {
run_command "$@"
}
main "$@"
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main "$@"
fi
+22 -4
View File
@@ -465,11 +465,25 @@ send text.flood checking ridge link
#### View or change battery alert state
**Usage:**
- `get battery.alert`
- `set battery.alert <on|off>`
- `get battery.alert.region`
- `set battery.alert on [region]`
- `set battery.alert off`
**Default:** `off`
**Parameters:**
- `region`: Optional named region scope. When omitted, the repeater selects the single deepest (most narrow) named region in the configured hierarchy. If multiple regions tie for deepest, specify one explicitly.
**Note:** When enabled, the repeater checks battery level once per minute and sends low-battery warnings to the `#repeaters` flood channel.
**Defaults:**
- `battery.alert`: `off`
- `battery.alert.region`: `<unset>`
**Notes:**
- Enabling fails until at least one usable named region is defined. Alerts are never sent as unscoped floods. If the selected region is later removed, alerts stop until battery alerts are enabled again with a valid region.
- Region hierarchy edits are not persistent until `region save` is run. After `region def west pnw wa w-wa sea`, run `region save` before enabling the alert if the hierarchy must survive a reboot.
- A region must have a usable transport key. Public named regions derive one automatically; a private region without an available key is rejected.
- The first alert is suppressed until the repeater has been up for at least 30 minutes. After that, the repeater checks every 30 minutes and sends low-battery warnings to the `#repeaters` channel in the selected region.
- With `region def west pnw wa w-wa sea`, `set battery.alert on` selects `sea`; `set battery.alert on w-wa` overrides that default.
- `get battery.alert.region` returns the selected scope, for example `> sea`.
- The battery check never requests a wake earlier than its 30-minute deadline. If the normal loop is already awake when that deadline has elapsed, the check is effectively free of an additional wake. Time in light/event sleep counts toward the startup delay, and a queued alert keeps the repeater awake until the packet is handled.
---
@@ -1378,6 +1392,10 @@ set flood.retry.ignore none
Flood retry timing retains its fixed maximum-frame plus 20 packet-airtime wait, then adds a random `0-200%` of one additional packet airtime on every attempt. This de-synchronizes repeaters that may have missed the same echo while capping the added wait at two frames.
Only one active retry sequence is kept for a given logical flood packet. An identical flood can still transmit normally, but it does not create a second sequence of extra attempts. Retry state is released if a queued packet is evicted, and the final echo window retains metadata without reserving a packet-pool entry.
Bridge reachability learned from earlier hops in a successful echo is cached separately from `recent.repeater`. Only the final RF hop updates `recent.repeater` and its SNR, so indirect path entries cannot affect direct-retry SNR gating or coding-rate selection.
**Examples:**
```
get flood.retry.bridge
@@ -1833,7 +1851,7 @@ Requires WiFi connected and the MQTT bridge running.
- `set bridge.secret <secret>`
**Parameters:**
- `secret`: ESP-NOW bridge secret, up to 15 characters
- `secret`: ESP-NOW bridge secret, 1-15 characters
**Default:** Varies by board
+33 -4
View File
@@ -73,7 +73,7 @@ set flood.retry.ignore none
| Setting | What it does | How to use | Example |
| --- | --- | --- | --- |
| `battery.alert` | Sends opt-in low-battery warnings to `#repeaters`. | `get battery.alert`, `set battery.alert on/off` | `set battery.alert on` |
| `battery.alert` | Sends opt-in, region-scoped low-battery warnings to `#repeaters` after 30 minutes of uptime. | `get battery.alert`, `get battery.alert.region`, `set battery.alert on [region]`, `set battery.alert off` | `set battery.alert on sea` |
| `battery.alert.low` | Warning threshold percentage. Must be greater than `battery.alert.critical`. | `get battery.alert.low`, `set battery.alert.low <1-100>` | `set battery.alert.low 20` |
| `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` |
@@ -92,9 +92,18 @@ set flood.retry.ignore none
## Battery Alerts
Battery alerts are off by default. When enabled, the repeater checks once per
minute and sends a flood text warning to `#repeaters` when voltage is above
`1 V` and the estimated battery percent is below `battery.alert.low`.
Battery alerts are off by default. Enabling requires a named region. With no
region argument, the repeater selects the single deepest (most narrow) region
in the hierarchy; if multiple regions tie, the command asks for an explicit
region. For example, after `region def west pnw wa w-wa sea`, `set
battery.alert on` selects `sea`, while `set battery.alert on w-wa` overrides
the default. Alerts are never sent as unscoped floods, and removing the selected
region stops alerts until a valid scope is selected again.
The repeater suppresses alerts for its first 30 minutes of uptime. It then
checks every 30 minutes and sends a flood text warning to `#repeaters` when
voltage is above `1 V` and the estimated battery percent is below
`battery.alert.low`.
Warnings repeat every `24` hours, or every `12` hours when the estimate is
below `battery.alert.critical`.
@@ -104,6 +113,7 @@ Defaults:
| Setting | Default |
| --- | ---: |
| `battery.alert` | `off` |
| `battery.alert.region` | `<unset>` |
| `battery.alert.low` | `20` |
| `battery.alert.critical` | `10` |
@@ -114,8 +124,17 @@ set battery.alert.low 20
set battery.alert.critical 10
set battery.alert on
get battery.alert
get battery.alert.region
```
CPU power saving remains compatible with the check. The battery timer never
requests a wake earlier than its 30-minute deadline; when the normal loop is
already awake after that deadline, no additional wake is needed. Sleeping time
counts toward the startup delay, and an outbound warning prevents another sleep
until the queued packet has been handled. This is separate from RX duty-cycle
power saving, which only cycles the LoRa receiver and does not stop the main
loop's battery timer.
## Recent Repeater Table
Direct retry uses the recent repeater table when `direct.retry.heard` is `on`.
@@ -350,6 +369,16 @@ delay, then adds random jitter from zero to 200 percent of one additional packet
airtime. This keeps nearby repeaters from repeating a collision on fixed timing
while capping the added wait at two frames.
Only one enhanced retry sequence can be active for the same logical flood
packet. Identical floods still receive their normal transmission, but do not
multiply the extra attempts. Evicted queued retries release their bridge state,
and the final echo wait does not reserve a packet-pool entry.
Earlier path hops from a successful bridge echo refresh a separate per-bucket
reachability cache without an SNR value. Only the final hop, which actually sent
the received RF frame, updates `recent.repeater` and its SNR. Indirect path hops
therefore cannot change direct-retry SNR gating or coding-rate selection.
## Troubleshooting
If advert packets are still retrying:
+49 -10
View File
@@ -1179,6 +1179,11 @@ void MyMesh::startInterface(BaseSerialInterface &serial) {
}
void MyMesh::handleCmdFrame(size_t len) {
if (len == 0) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
if (cmd_frame[0] == CMD_DEVICE_QUERY && len >= 2) { // sent when app establishes connection
app_target_ver = cmd_frame[1]; // which version of protocol does app understand
@@ -1288,6 +1293,10 @@ void MyMesh::handleCmdFrame(size_t len) {
: ERR_CODE_UNSUPPORTED_CMD); // unknown recipient, or unsupported TXT_TYPE_*
}
} else if (cmd_frame[0] == CMD_SEND_CHANNEL_TXT_MSG) { // send GroupChannel text msg
if (len < 7) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
int i = 1;
uint8_t txt_type = cmd_frame[i++]; // should be TXT_TYPE_PLAIN
uint8_t channel_idx = cmd_frame[i++];
@@ -1308,7 +1317,7 @@ void MyMesh::handleCmdFrame(size_t len) {
}
}
} else if (cmd_frame[0] == CMD_SEND_CHANNEL_DATA) { // send GroupChannel datagram
if (len < 4) {
if (len < 3) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
@@ -1325,6 +1334,14 @@ void MyMesh::handleCmdFrame(size_t len) {
// parse provided path if not flood
uint8_t path[MAX_PATH_SIZE];
size_t path_bytes = 0;
if (path_len != OUT_PATH_UNKNOWN) {
path_bytes = (size_t)(path_len & 63) * (size_t)((path_len >> 6) + 1);
}
if ((size_t)i + path_bytes + 2 > len) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
if (path_len != OUT_PATH_UNKNOWN) {
i += mesh::Packet::writePath(path, &cmd_frame[i], path_len);
}
@@ -1460,7 +1477,7 @@ void MyMesh::handleCmdFrame(size_t len) {
writeErrFrame(ERR_CODE_TABLE_FULL);
}
}
} else if (cmd_frame[0] == CMD_REMOVE_CONTACT) {
} else if (cmd_frame[0] == CMD_REMOVE_CONTACT && len >= 1 + PUB_KEY_SIZE) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient && removeContact(*recipient)) {
@@ -1471,7 +1488,7 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_NOT_FOUND); // not found, or unable to remove
}
} else if (cmd_frame[0] == CMD_SHARE_CONTACT) {
} else if (cmd_frame[0] == CMD_SHARE_CONTACT && len >= 1 + PUB_KEY_SIZE) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient) {
@@ -1483,7 +1500,7 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_NOT_FOUND);
}
} else if (cmd_frame[0] == CMD_GET_CONTACT_BY_KEY) {
} else if (cmd_frame[0] == CMD_GET_CONTACT_BY_KEY && len >= 1 + PUB_KEY_SIZE) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *contact = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (contact) {
@@ -1540,6 +1557,10 @@ void MyMesh::handleCmdFrame(size_t len) {
_serial->writeFrame(out_frame, 1);
}
} else if (cmd_frame[0] == CMD_SET_RADIO_PARAMS) {
if (len < 11) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
int i = 1;
uint32_t freq;
memcpy(&freq, &cmd_frame[i], 4);
@@ -1579,6 +1600,10 @@ void MyMesh::handleCmdFrame(size_t len) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
}
} else if (cmd_frame[0] == CMD_SET_RADIO_TX_POWER) {
if (len < 2) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
int8_t power = (int8_t)cmd_frame[1];
if (power < -9 || power > MAX_LORA_TX_POWER) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
@@ -1589,6 +1614,10 @@ void MyMesh::handleCmdFrame(size_t len) {
writeOKFrame();
}
} else if (cmd_frame[0] == CMD_SET_TUNING_PARAMS) {
if (len < 9) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
int i = 1;
uint32_t rx, af;
memcpy(&rx, &cmd_frame[i], 4);
@@ -1607,6 +1636,10 @@ void MyMesh::handleCmdFrame(size_t len) {
memcpy(&out_frame[i], &af, 4); i += 4;
_serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_SET_OTHER_PARAMS) {
if (len < 2) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
_prefs.manual_add_contacts = cmd_frame[1];
if (len >= 3) {
_prefs.telemetry_mode_base = cmd_frame[2] & 0x03; // v5+
@@ -1623,7 +1656,7 @@ void MyMesh::handleCmdFrame(size_t len) {
updateGpsTelemetryPolicy();
savePrefs();
writeOKFrame();
} else if (cmd_frame[0] == CMD_SET_PATH_HASH_MODE && cmd_frame[1] == 0 && len >= 3) {
} else if (cmd_frame[0] == CMD_SET_PATH_HASH_MODE && len >= 3 && cmd_frame[1] == 0) {
if (cmd_frame[2] >= 3) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
@@ -1631,7 +1664,7 @@ void MyMesh::handleCmdFrame(size_t len) {
savePrefs();
writeOKFrame();
}
} else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) {
} else if (cmd_frame[0] == CMD_REBOOT && len >= 7 && memcmp(&cmd_frame[1], "reboot", 6) == 0) {
if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed?
saveContacts();
}
@@ -1767,7 +1800,7 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
}
} else if (cmd_frame[0] == CMD_SEND_PATH_DISCOVERY_REQ && cmd_frame[1] == 0 && len >= 2 + PUB_KEY_SIZE) {
} else if (cmd_frame[0] == CMD_SEND_PATH_DISCOVERY_REQ && len >= 2 + PUB_KEY_SIZE && cmd_frame[1] == 0) {
uint8_t *pub_key = &cmd_frame[2];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient) {
@@ -2100,7 +2133,7 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid stats sub-type
}
} else if (cmd_frame[0] == CMD_FACTORY_RESET && memcmp(&cmd_frame[1], "reset", 5) == 0) {
} else if (cmd_frame[0] == CMD_FACTORY_RESET && len >= 6 && memcmp(&cmd_frame[1], "reset", 5) == 0) {
if (_serial) {
MESH_DEBUG_PRINTLN("Factory reset: disabling serial interface to prevent reconnects (BLE/WiFi)");
_serial->disable(); // Phone app disconnects before we can send OK frame so it's safe here
@@ -2126,9 +2159,11 @@ void MyMesh::handleCmdFrame(size_t len) {
writeOKFrame();
} else if (cmd_frame[0] == CMD_SET_DEFAULT_FLOOD_SCOPE && len >= 1) {
if (len >= 1+31+16) {
int n = strlen((char *) &cmd_frame[1]);
const void* terminator = memchr(&cmd_frame[1], 0, 31);
size_t n = terminator == NULL ? 31 : (const uint8_t*)terminator - &cmd_frame[1];
if (n > 0 && n < 31) {
strcpy(_prefs.default_scope_name, (char *) &cmd_frame[1]);
memcpy(_prefs.default_scope_name, &cmd_frame[1], n);
_prefs.default_scope_name[n] = 0;
memcpy(_prefs.default_scope_key, &cmd_frame[1+31], 16);
savePrefs();
writeOKFrame();
@@ -2159,6 +2194,10 @@ void MyMesh::handleCmdFrame(size_t len) {
writeErrFrame(ERR_CODE_TABLE_FULL);
}
} else if (cmd_frame[0] == CMD_SET_AUTOADD_CONFIG) {
if (len < 2) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
_prefs.autoadd_config = cmd_frame[1];
if (len >= 3) {
_prefs.autoadd_max_hops = min(cmd_frame[2], (uint8_t)64);
+249 -57
View File
@@ -101,7 +101,8 @@
#endif
#define LOW_BATTERY_MIN_VALID_MV 1000
#define LOW_BATTERY_CHECK_INTERVAL (60UL * 1000UL)
#define LOW_BATTERY_STARTUP_DELAY (30ULL * 60ULL * 1000ULL)
#define LOW_BATTERY_CHECK_INTERVAL (30UL * 60UL * 1000UL)
#define LOW_BATTERY_WARN_INTERVAL (24UL * 60UL * 60UL * 1000UL)
#define LOW_BATTERY_CRITICAL_INTERVAL (12UL * 60UL * 60UL * 1000UL)
@@ -315,6 +316,10 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr
}
client = acl.putClient(sender, 0); // add to contacts (if not already known)
if (client == NULL) {
MESH_DEBUG_PRINTLN("Login rejected: ACL is full of protected contacts");
return 0;
}
if (sender_timestamp <= client->last_timestamp) {
MESH_DEBUG_PRINTLN("Possible login replay attack!");
return 0; // FATAL: client table is full -OR- replay attack
@@ -1391,6 +1396,61 @@ uint8_t MyMesh::floodRetrySourceMask(const mesh::Packet* packet) const {
return floodRetryBucketMaskForPrefix(source_prefix, hash_size, true);
}
bool MyMesh::floodRetryBridgeBucketFresh(uint8_t bucket) const {
if (bucket > FLOOD_RETRY_BRIDGE_OTHER_BUCKET) {
return false;
}
const uint8_t (*prefixes)[FLOOD_RETRY_PREFIX_LEN];
uint8_t prefix_count;
if (bucket == FLOOD_RETRY_BRIDGE_OTHER_BUCKET) {
prefixes = _prefs.flood_retry_prefixes;
prefix_count = FLOOD_RETRY_PREFIX_SLOTS;
} else {
prefixes = _prefs.flood_retry_bridge_buckets[bucket];
prefix_count = FLOOD_RETRY_BUCKET_PREFIXES;
}
for (uint8_t i = 0; i < prefix_count; i++) {
const uint8_t* configured = prefixes[i];
if ((configured[0] != 0 || configured[1] != 0 || configured[2] != 0)
&& !floodRetryPrefixIgnored(configured, FLOOD_RETRY_PREFIX_LEN)
&& floodRetryPrefixFresh(configured, FLOOD_RETRY_PREFIX_LEN)) {
return true;
}
}
const FloodRetryBridgeReachability& reachable = flood_retry_bridge_reachability[bucket];
if (reachable.prefix_len == 0 || reachable.last_heard_millis == 0
|| (uint32_t)(millis() - reachable.last_heard_millis) > 3600000UL) {
return false;
}
return (floodRetryBucketMaskForPrefix(reachable.prefix, reachable.prefix_len, false)
& floodRetryBucketMask(bucket)) != 0;
}
void MyMesh::recordFloodRetryBridgeReachability(const uint8_t* prefix, uint8_t prefix_len,
uint8_t bucket_mask) {
if (prefix == NULL || prefix_len == 0 || prefix_len > MAX_ROUTE_HASH_BYTES) {
return;
}
uint32_t now = millis();
if (now == 0) {
now = 1; // zero means unused
}
for (uint8_t bucket = 0; bucket <= FLOOD_RETRY_BRIDGE_OTHER_BUCKET; bucket++) {
if ((bucket_mask & floodRetryBucketMask(bucket)) == 0) {
continue;
}
FloodRetryBridgeReachability& reachable = flood_retry_bridge_reachability[bucket];
memset(reachable.prefix, 0, sizeof(reachable.prefix));
memcpy(reachable.prefix, prefix, prefix_len);
reachable.prefix_len = prefix_len;
reachable.last_heard_millis = now;
}
}
uint8_t MyMesh::floodRetryBridgeTargetMask(uint8_t source_mask) const {
uint8_t mask = 0;
for (int bucket = 0; bucket < FLOOD_RETRY_BRIDGE_BUCKETS; bucket++) {
@@ -1398,27 +1458,14 @@ uint8_t MyMesh::floodRetryBridgeTargetMask(uint8_t source_mask) const {
if ((source_mask & bucket_mask) != 0) {
continue;
}
for (int i = 0; i < FLOOD_RETRY_BUCKET_PREFIXES; i++) {
const uint8_t* configured = _prefs.flood_retry_bridge_buckets[bucket][i];
if ((configured[0] != 0 || configured[1] != 0 || configured[2] != 0)
&& !floodRetryPrefixIgnored(configured, FLOOD_RETRY_PREFIX_LEN)
&& floodRetryPrefixFresh(configured, FLOOD_RETRY_PREFIX_LEN)) {
mask |= bucket_mask;
break;
}
if (floodRetryBridgeBucketFresh((uint8_t)bucket)) {
mask |= bucket_mask;
}
}
uint8_t other_mask = floodRetryBucketMask(FLOOD_RETRY_BRIDGE_OTHER_BUCKET);
if ((source_mask & other_mask) == 0) {
for (int i = 0; i < FLOOD_RETRY_PREFIX_SLOTS; i++) {
const uint8_t* configured = _prefs.flood_retry_prefixes[i];
if ((configured[0] != 0 || configured[1] != 0 || configured[2] != 0)
&& !floodRetryPrefixIgnored(configured, FLOOD_RETRY_PREFIX_LEN)
&& floodRetryPrefixFresh(configured, FLOOD_RETRY_PREFIX_LEN)) {
mask |= other_mask;
break;
}
}
if ((source_mask & other_mask) == 0
&& floodRetryBridgeBucketFresh(FLOOD_RETRY_BRIDGE_OTHER_BUCKET)) {
mask |= other_mask;
}
return mask;
}
@@ -1515,14 +1562,20 @@ bool MyMesh::allowFloodRetry(const mesh::Packet* packet) const {
return true;
}
void MyMesh::clearFloodRetryBridgeState(const mesh::Packet* packet) {
FloodRetryBridgeState* state = floodRetryBridgeStateFor(packet, false);
if (state != NULL) {
state->active = false;
void MyMesh::clearFloodRetryBridgeStateByKey(const uint8_t* retry_key) {
if (retry_key == NULL) {
return;
}
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
if (flood_retry_bridge_states[i].active
&& memcmp(flood_retry_bridge_states[i].key, retry_key, MAX_HASH_SIZE) == 0) {
flood_retry_bridge_states[i].active = false;
return;
}
}
}
void MyMesh::refreshFloodRetryHeardRecent(const mesh::Packet* packet) {
void MyMesh::refreshFloodRetryReachability(const mesh::Packet* packet) {
if (packet == NULL || !packet->isRouteFlood() || packet->getPathHashCount() == 0) {
return;
}
@@ -1536,11 +1589,16 @@ void MyMesh::refreshFloodRetryHeardRecent(const mesh::Packet* packet) {
if (tables == NULL) {
return;
}
uint8_t path_count = packet->getPathHashCount();
const uint8_t* last_hop = &packet->path[(path_count - 1) * hash_size];
tables->setRecentRepeater(last_hop, hash_size, packet->_snr, false, true);
const uint8_t* path = packet->path;
if (_prefs.flood_retry_bridge_enabled) {
FloodRetryBridgeState* state = floodRetryBridgeStateFor(packet, false);
if (state != NULL) {
for (int hop = 0; hop < packet->getPathHashCount(); hop++) {
for (uint8_t hop = 0; hop < path_count; hop++) {
if (state->progress_marker > 0 && hop == state->progress_marker - 1) {
path += hash_size;
continue;
@@ -1548,17 +1606,13 @@ void MyMesh::refreshFloodRetryHeardRecent(const mesh::Packet* packet) {
uint8_t bucket_mask = floodRetryBucketMaskForPathHop(path, hash_size, (uint8_t)hop,
state->progress_marker);
bucket_mask &= state->target_mask & (uint8_t)~state->source_mask;
if (bucket_mask != 0) {
tables->setRecentRepeater(path, hash_size, packet->_snr, false, true);
if (bucket_mask != 0 && hop != path_count - 1) {
recordFloodRetryBridgeReachability(path, hash_size, bucket_mask);
}
path += hash_size;
}
return;
}
}
const uint8_t* heard_prefix = &packet->path[(packet->getPathHashCount() - 1) * hash_size];
tables->setRecentRepeater(heard_prefix, hash_size, packet->_snr, false, true);
}
void MyMesh::formatFloodRetryPath(char* dest, size_t dest_len, const mesh::Packet* packet) const {
@@ -1676,19 +1730,26 @@ bool MyMesh::formatFloodRetryHeard(char* dest, size_t dest_len, const mesh::Pack
}
void MyMesh::onFloodRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) {
if (event == NULL || packet == NULL) {
if (event == NULL) {
return;
}
bool clear_bridge_state = _prefs.flood_retry_bridge_enabled
&& (strcmp(event, "good") == 0 || strcmp(event, "failure") == 0 || strcmp(event, "failed_all_tries") == 0
|| strncmp(event, "dropped_", 8) == 0);
if (clear_bridge_state && strcmp(event, "failure") == 0) {
clearFloodRetryBridgeState(packet);
if (strcmp(event, "failure") == 0) {
return;
}
if (strcmp(event, "failure") == 0) {
if (packet == NULL) {
MESH_DEBUG_PRINTLN("flood retry %s (retry=%u, elapsed_ms=%lu, packet=released)",
event, (unsigned int)retry_attempt, (unsigned long)delay_millis);
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
if (f) {
f.print(getLogDateTime());
f.printf(": FLOOD RETRY %s (retry=%u, elapsed_ms=%lu, packet=released)\n",
event, (unsigned int)retry_attempt, (unsigned long)delay_millis);
f.close();
}
}
return;
}
@@ -1708,7 +1769,7 @@ void MyMesh::onFloodRetryEvent(const char* event, const mesh::Packet* packet, ui
formatFloodRetryPath(path_log, sizeof(path_log), packet);
heard_suffix[0] = 0;
if (strcmp(event, "good") == 0 && formatFloodRetryHeard(heard_log, sizeof(heard_log), packet)) {
refreshFloodRetryHeardRecent(packet);
refreshFloodRetryReachability(packet);
snprintf(heard_suffix, sizeof(heard_suffix), ", heard=%s", heard_log);
}
uint8_t log_cr = getRetryLogCodingRate(packet, getDefaultTxCodingRate());
@@ -1749,9 +1810,10 @@ void MyMesh::onFloodRetryEvent(const char* event, const mesh::Packet* packet, ui
}
}
if (clear_bridge_state) {
clearFloodRetryBridgeState(packet);
}
}
void MyMesh::onFloodRetrySlotReleased(const uint8_t* retry_key) {
clearFloodRetryBridgeStateByKey(retry_key);
}
bool MyMesh::hasFloodRetryTargetPrefix(const mesh::Packet* packet) const {
@@ -2247,6 +2309,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_logging = false;
region_load_active = false;
memset(flood_retry_bridge_states, 0, sizeof(flood_retry_bridge_states));
memset(flood_retry_bridge_reachability, 0, sizeof(flood_retry_bridge_reachability));
recv_pkt_region = NULL;
memset(flood_channel_blocks, 0, sizeof(flood_channel_blocks));
@@ -2445,14 +2508,14 @@ void MyMesh::begin(FILESYSTEM *fs) {
#endif
}
void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size) {
bool MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size) {
if (scope.isNull()) {
sendFlood(pkt, delay_millis, path_hash_size);
return sendFlood(pkt, delay_millis, path_hash_size);
} else {
uint16_t codes[2];
codes[0] = scope.calcTransportCode(pkt);
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
sendFlood(pkt, codes, delay_millis, path_hash_size);
return sendFlood(pkt, codes, delay_millis, path_hash_size);
}
}
@@ -2477,7 +2540,66 @@ bool MyMesh::resolveAlertScope(TransportKey& dest) {
return false;
}
bool MyMesh::sendRepeatersFloodText(const char* text) {
const RegionEntry* MyMesh::findNarrowestBatteryAlertRegion(bool& ambiguous) {
ambiguous = false;
const RegionEntry* narrowest = NULL;
uint8_t narrowest_depth = 0;
const int region_count = region_map.getCount();
for (int i = 0; i < region_count; i++) {
const RegionEntry* candidate = region_map.getByIdx(i);
uint8_t depth = 1;
uint16_t parent_id = candidate->parent;
bool valid = true;
// Region files are validated when loaded, but the live map can be edited
// before it is saved. Bound the walk so a temporary cycle cannot hang the
// repeater while selecting the default battery-alert scope.
for (int hops = 0; parent_id != 0; hops++) {
if (hops >= region_count) {
valid = false;
break;
}
const RegionEntry* parent = region_map.findById(parent_id);
if (parent == NULL || parent->isWildcard()) {
valid = false;
break;
}
depth++;
parent_id = parent->parent;
}
if (!valid) continue;
// A named region is not necessarily a usable transport scope. In
// particular, private regions need a stored key; do not select one merely
// because it happens to be the deepest entry in the hierarchy.
TransportKey candidate_scope;
if (!getBatteryAlertScopeForRegion(*candidate, candidate_scope)) continue;
if (depth > narrowest_depth) {
narrowest = candidate;
narrowest_depth = depth;
ambiguous = false;
} else if (depth == narrowest_depth) {
ambiguous = true;
}
}
return narrowest;
}
bool MyMesh::getBatteryAlertScopeForRegion(const RegionEntry& region, TransportKey& scope) {
if (region.isWildcard()) return false;
return region_map.getTransportKeysFor(region, &scope, 1) > 0 && !scope.isNull();
}
bool MyMesh::resolveBatteryAlertScope(TransportKey& scope) {
if (_prefs.battery_alert_region[0] == 0) return false;
const RegionEntry* region = region_map.findByName(_prefs.battery_alert_region);
return region != NULL && getBatteryAlertScopeForRegion(*region, scope);
}
bool MyMesh::sendRepeatersFloodText(const char* text, const TransportKey* scope) {
if (text == NULL || *text == 0) return false;
mesh::GroupChannel channel;
@@ -2521,8 +2643,8 @@ bool MyMesh::sendRepeatersFloodText(const char* text) {
return false;
}
sendFloodScoped(default_scope, pkt, 0, _prefs.path_hash_mode + 1);
return true;
const TransportKey& send_scope = scope == NULL ? default_scope : *scope;
return sendFloodScoped(send_scope, pkt, 0, _prefs.path_hash_mode + 1);
}
void MyMesh::checkBatteryAlert() {
@@ -2531,11 +2653,26 @@ void MyMesh::checkBatteryAlert() {
return;
}
// Ignore startup voltage sag and give solar/charger hardware time to settle.
// uptime_millis is 64-bit and includes time spent in the platform's light or
// event sleep, so this guard remains reliable across millis() wraparound.
if (uptime_millis < LOW_BATTERY_STARTUP_DELAY) {
return;
}
if (next_battery_alert_check && !millisHasNowPassed(next_battery_alert_check)) {
return;
}
next_battery_alert_check = futureMillis(LOW_BATTERY_CHECK_INTERVAL);
// Check the cheap configuration path first. This avoids powering the ADC or
// battery-divider circuitry when the selected region has been removed or no
// longer has a usable transport key.
TransportKey alert_scope;
if (!resolveBatteryAlertScope(alert_scope)) {
return; // low-battery alerts are never sent as an unscoped flood
}
uint16_t batt_mv = board.getBattMilliVolts();
uint8_t batt_pct = batteryPercentFromMilliVolts(batt_mv);
if (batt_mv <= LOW_BATTERY_MIN_VALID_MV || batt_pct >= _prefs.battery_alert_low_percent) {
@@ -2552,7 +2689,7 @@ void MyMesh::checkBatteryAlert() {
char text[96];
snprintf(text, sizeof(text), "LOW BATTERY %u%% (%u mV)", (uint32_t)batt_pct, (uint32_t)batt_mv);
if (sendRepeatersFloodText(text)) {
if (sendRepeatersFloodText(text, &alert_scope)) {
battery_alert_sent = true;
last_battery_alert_sent = millis();
}
@@ -2579,6 +2716,19 @@ bool MyMesh::hasStartedScheduledTempRadio() const {
return false;
}
#if defined(ENABLE_OTA)
bool MyMesh::isTempRadioActive() const {
const uint32_t now = getRTCClock()->getCurrentTime();
for (int i = 0; i < MAX_SCHEDULED_RADIO_SETTINGS; i++) {
const ScheduledRadioSetting& setting = scheduled_radio_settings[i];
if (setting.active && setting.temporary && setting.started && now < setting.end_time) {
return true;
}
}
return false;
}
#endif
int MyMesh::findFreeScheduledRadioSlot() const {
for (int i = 0; i < MAX_SCHEDULED_RADIO_SETTINGS; i++) {
if (!scheduled_radio_settings[i].active) {
@@ -2921,7 +3071,8 @@ void MyMesh::processScheduledRadioSettings() {
for (int i = 0; i < MAX_SCHEDULED_RADIO_SETTINGS; i++) {
ScheduledRadioSetting& setting = scheduled_radio_settings[i];
if (setting.active && setting.temporary && setting.started && now >= setting.end_time) {
if (setting.active && setting.temporary && setting.started && now >= setting.end_time
&& !hasOutbound()) {
setting.active = false;
setting.started = false;
temp_ended = true;
@@ -3180,6 +3331,7 @@ void MyMesh::formatNeighborsReply(char *reply) {
void MyMesh::removeNeighbor(const uint8_t *pubkey, int key_len) {
#if MAX_NEIGHBOURS
if (pubkey == NULL || key_len <= 0 || key_len > PUB_KEY_SIZE) return;
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
NeighbourInfo *neighbour = &neighbours[i];
if (memcmp(neighbour->id.pub_key, pubkey, key_len) == 0) {
@@ -3768,13 +3920,14 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
if (sp == NULL) {
strcpy(reply, "Err - bad params");
} else {
size_t hex_len = (size_t)(sp - hex);
*sp++ = 0; // replace space with null terminator
uint8_t pubkey[PUB_KEY_SIZE];
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
if (hex_len > 0 && hex_len <= PUB_KEY_SIZE * 2 && (hex_len & 1) == 0
&& mesh::Utils::fromHex(pubkey, (int)(hex_len / 2), hex)) {
uint8_t perms = atoi(sp);
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
if (acl.applyPermissions(self_id, pubkey, (int)(hex_len / 2), perms)) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save()
strcpy(reply, "OK");
} else {
@@ -3849,24 +4002,63 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
}
} else if (strcmp(command, "get battery.alert") == 0) {
sprintf(reply, "> %s", _prefs.battery_alert_enabled ? "on" : "off");
} else if (strcmp(command, "get battery.alert.region") == 0) {
sprintf(reply, "> %s", _prefs.battery_alert_region[0]
? _prefs.battery_alert_region : "<unset>");
} else if (strcmp(command, "get battery.alert.low") == 0) {
sprintf(reply, "> %u", (uint32_t)_prefs.battery_alert_low_percent);
} else if (strcmp(command, "get battery.alert.critical") == 0) {
sprintf(reply, "> %u", (uint32_t)_prefs.battery_alert_critical_percent);
} else if (strncmp(command, "set battery.alert ", 18) == 0) {
const char* value = command + 18;
if (strcmp(value, "on") == 0) {
if (strncmp(value, "on", 2) == 0 && (value[2] == 0 || value[2] == ' ')) {
const char* region_name = skipLocalSpaces(value + 2);
const RegionEntry* region = NULL;
bool ambiguous = false;
if (*region_name) {
if (strchr(region_name, ' ') != NULL) {
strcpy(reply, "Err - region names cannot contain spaces");
return;
}
region = region_map.findByName(region_name);
if (region == NULL || region->isWildcard()) {
strcpy(reply, "Err - unknown or invalid alert region");
return;
}
} else {
region = findNarrowestBatteryAlertRegion(ambiguous);
if (region == NULL) {
strcpy(reply, "Err - define a usable region before enabling battery alerts");
return;
}
if (ambiguous) {
strcpy(reply, "Err - multiple narrowest regions; specify one");
return;
}
}
TransportKey region_scope;
if (!getBatteryAlertScopeForRegion(*region, region_scope)) {
strcpy(reply, "Err - alert region has no usable transport key");
return;
}
StrHelper::strncpy(_prefs.battery_alert_region, region->name,
sizeof(_prefs.battery_alert_region));
_prefs.battery_alert_enabled = 1;
next_battery_alert_check = 0;
battery_alert_sent = false;
savePrefs();
strcpy(reply, "OK");
sprintf(reply, "OK - battery alerts scoped to %s", _prefs.battery_alert_region);
} else if (strcmp(value, "off") == 0) {
_prefs.battery_alert_enabled = 0;
next_battery_alert_check = 0;
battery_alert_sent = false;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - usage: set battery.alert <on|off>");
strcpy(reply, "Err - usage: set battery.alert <on [region]|off>");
}
} else if (strncmp(command, "set battery.alert.low ", 22) == 0) {
uint8_t percent;
+17 -5
View File
@@ -143,6 +143,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
uint8_t progress_marker;
bool active;
};
struct FloodRetryBridgeReachability {
uint8_t prefix[MAX_ROUTE_HASH_BYTES];
uint8_t prefix_len;
uint32_t last_heard_millis;
};
struct FloodChannelBlockEntry {
bool active;
uint8_t key_len;
@@ -152,6 +157,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
char name[FLOOD_CHANNEL_BLOCK_NAME_LEN];
};
mutable FloodRetryBridgeState flood_retry_bridge_states[MAX_FLOOD_RETRY_SLOTS];
FloodRetryBridgeReachability flood_retry_bridge_reachability[FLOOD_RETRY_BRIDGE_BUCKETS + 1];
FloodChannelBlockEntry flood_channel_blocks[FLOOD_CHANNEL_BLOCK_SLOTS];
uint32_t pending_discover_tag;
unsigned long pending_discover_until;
@@ -212,12 +218,14 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
uint8_t floodRetryBucketMaskForPathHop(const uint8_t* prefix, uint8_t prefix_len, uint8_t hop,
uint8_t progress_marker) const;
uint8_t floodRetrySourceMask(const mesh::Packet* packet) const;
bool floodRetryBridgeBucketFresh(uint8_t bucket) const;
void recordFloodRetryBridgeReachability(const uint8_t* prefix, uint8_t prefix_len, uint8_t bucket_mask);
uint8_t floodRetryBridgeTargetMask(uint8_t source_mask) const;
uint8_t floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t source_mask,
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 clearFloodRetryBridgeStateByKey(const uint8_t* retry_key);
void refreshFloodRetryReachability(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);
@@ -227,7 +235,10 @@ 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);
bool sendRepeatersFloodText(const char* text, const TransportKey* scope = nullptr);
const RegionEntry* findNarrowestBatteryAlertRegion(bool& ambiguous);
bool getBatteryAlertScopeForRegion(const RegionEntry& region, TransportKey& scope);
bool resolveBatteryAlertScope(TransportKey& scope);
void checkBatteryAlert();
void printRecentRepeatersSerial();
@@ -268,7 +279,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
protected:
#if defined(ENABLE_OTA)
bool isTempRadioActive() const override { return hasStartedScheduledTempRadio(); }
bool isTempRadioActive() const override;
#endif
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;
@@ -299,6 +310,7 @@ protected:
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;
void onFloodRetrySlotReleased(const uint8_t* retry_key) 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;
@@ -359,7 +371,7 @@ public:
_cli.savePrefs(_fs);
}
void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
bool sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
// CommonCLICallbacks
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
+9 -4
View File
@@ -360,6 +360,10 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
}
client = acl.putClient(sender, 0); // add to known clients (if not already known)
if (client == NULL) {
MESH_DEBUG_PRINTLN("Login rejected: ACL is full of protected contacts");
return;
}
if (sender_timestamp <= client->last_timestamp) {
MESH_DEBUG_PRINTLN("possible replay attack!");
return;
@@ -1013,13 +1017,14 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
if (sp == NULL) {
strcpy(reply, "Err - bad params");
} else {
size_t hex_len = (size_t)(sp - hex);
*sp++ = 0; // replace space with null terminator
uint8_t pubkey[PUB_KEY_SIZE];
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
if (hex_len > 0 && hex_len <= PUB_KEY_SIZE * 2 && (hex_len & 1) == 0
&& mesh::Utils::fromHex(pubkey, (int)(hex_len / 2), hex)) {
uint8_t perms = atoi(sp);
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
if (acl.applyPermissions(self_id, pubkey, (int)(hex_len / 2), perms)) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save()
strcpy(reply, "OK");
} else {
@@ -1120,7 +1125,7 @@ void MyMesh::loop() {
MESH_DEBUG_PRINTLN("Temp radio params");
}
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
if (revert_radio_at && millisHasNowPassed(revert_radio_at) && !hasOutbound()) { // revert radio params to orig
revert_radio_at = 0; // clear timer
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
MESH_DEBUG_PRINTLN("Radio params restored");
+3 -1
View File
@@ -140,7 +140,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
protected:
#if defined(ENABLE_OTA)
bool isTempRadioActive() const override { return set_radio_at == 0 && revert_radio_at != 0; }
bool isTempRadioActive() const override {
return set_radio_at == 0 && revert_radio_at != 0 && !millisHasNowPassed(revert_radio_at);
}
#endif
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;
+3
View File
@@ -127,6 +127,9 @@ void loop() {
#endif
if (the_mesh.getNodePrefs()->powersaving_enabled && !board.isUsbDataConnected()) {
uint32_t sleep_secs = the_mesh.getPowerSaveSleepSeconds(30);
#ifdef HAS_EXTERNAL_WATCHDOG
if (sleep_secs > 0) external_watchdog.feed();
#endif
#if defined(NRF52_PLATFORM)
if (sleep_secs > 0) {
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
+9 -4
View File
@@ -383,6 +383,10 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
}
client = acl.putClient(sender, PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO); // add to contacts (if not already known)
if (client == NULL) {
MESH_DEBUG_PRINTLN("Login rejected: ACL is full of protected contacts");
return 0;
}
if (sender_timestamp <= client->last_timestamp) {
MESH_DEBUG_PRINTLN("Possible login replay attack!");
return 0; // FATAL: client table is full -OR- replay attack
@@ -435,13 +439,14 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
if (sp == NULL) {
strcpy(reply, "Err - bad params");
} else {
size_t hex_len = (size_t)(sp - hex);
*sp++ = 0; // replace space with null terminator
uint8_t pubkey[PUB_KEY_SIZE];
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
if (hex_len > 0 && hex_len <= PUB_KEY_SIZE * 2 && (hex_len & 1) == 0
&& mesh::Utils::fromHex(pubkey, (int)(hex_len / 2), hex)) {
uint8_t perms = atoi(sp);
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
if (acl.applyPermissions(self_id, pubkey, (int)(hex_len / 2), perms)) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save()
updateGpsTelemetryPolicy();
strcpy(reply, "OK");
@@ -970,7 +975,7 @@ void SensorMesh::loop() {
MESH_DEBUG_PRINTLN("Temp radio params");
}
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
if (revert_radio_at && millisHasNowPassed(revert_radio_at) && !hasOutbound()) { // revert radio params to orig
revert_radio_at = 0; // clear timer
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
MESH_DEBUG_PRINTLN("Radio params restored");
+3 -1
View File
@@ -85,7 +85,9 @@ public:
protected:
#if defined(ENABLE_OTA)
bool isTempRadioActive() const override { return set_radio_at == 0 && revert_radio_at != 0; }
bool isTempRadioActive() const override {
return set_radio_at == 0 && revert_radio_at != 0 && !millisHasNowPassed(revert_radio_at);
}
#endif
// current telemetry data queries
float getVoltage(uint8_t channel) { return getTelemValue(channel, LPP_VOLTAGE); }
+29 -14
View File
@@ -29,6 +29,12 @@ void Dispatcher::begin() {
_radio->begin();
prev_isrecv_mode = _radio->isInRecvMode();
#ifdef WITH_MQTT_BRIDGE
// Use begin() as the watchdog baseline even if the radio never reports an
// RX/IRQ timestamp. This lets a radio that is dead from startup recover.
last_radio_active_ms = _ms->getMillis();
last_watchdog_recovery = last_radio_active_ms;
#endif
}
float Dispatcher::getAirtimeBudgetFactor() const {
@@ -107,20 +113,20 @@ void Dispatcher::loop() {
{
const uint32_t watchdog_ms = getRadioWatchdogMillis();
if (watchdog_ms > 0) {
unsigned long last_recv = _radio->getLastRecvMillis();
unsigned long last_irq = _radio->getLastRadioInterruptMillis();
unsigned long last_active = (last_recv > last_irq ? last_recv : last_irq);
if (last_radio_active_ms > last_active) last_active = last_radio_active_ms;
if (is_recv && last_active > 0) {
unsigned long silent_ms = _ms->getMillis() - last_active;
unsigned long since_recovery = _ms->getMillis() - last_watchdog_recovery;
if (silent_ms > watchdog_ms && since_recovery > watchdog_ms) {
_err_flags |= ERR_EVENT_RADIO_WATCHDOG;
MESH_DEBUG_PRINTLN("Radio watchdog: silent %lu ms, state=%d, recovering", silent_ms, _radio->getRadioState());
_radio->idle();
_radio->startRecv();
last_watchdog_recovery = _ms->getMillis();
}
const unsigned long now = _ms->getMillis();
unsigned long silent_ms = now - last_radio_active_ms;
const unsigned long last_recv = _radio->getLastRecvMillis();
const unsigned long last_irq = _radio->getLastRadioInterruptMillis();
if (last_recv != 0 && now - last_recv < silent_ms) silent_ms = now - last_recv;
if (last_irq != 0 && now - last_irq < silent_ms) silent_ms = now - last_irq;
const unsigned long since_recovery = now - last_watchdog_recovery;
if (is_recv && silent_ms >= watchdog_ms && since_recovery >= watchdog_ms) {
_err_flags |= ERR_EVENT_RADIO_WATCHDOG;
MESH_DEBUG_PRINTLN("Radio watchdog: silent %lu ms, state=%d, recovering", silent_ms, _radio->getRadioState());
_radio->idle();
_radio->startRecv();
last_watchdog_recovery = now;
last_radio_active_ms = now;
}
}
}
@@ -358,6 +364,15 @@ void Dispatcher::checkSend() {
outbound = _mgr->getNextOutbound(_ms->getMillis());
if (outbound) {
if (!allowPacketTransmit(outbound)) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): packet no longer allowed, type=%u", getLogDateTime(),
(uint32_t)outbound->getPayloadType());
onSendFail(outbound);
releasePacket(outbound);
outbound = NULL;
return;
}
int len = 0;
uint8_t raw[MAX_TRANS_UNIT];
+1
View File
@@ -207,6 +207,7 @@ protected:
virtual uint32_t getCADFailRetryDelay() const;
virtual uint32_t getCADFailMaxDuration() const;
virtual uint8_t getDefaultTxCodingRate() const { return 0; }
virtual bool allowPacketTransmit(const Packet* packet) const { return true; }
virtual int getInterferenceThreshold() const { return 0; } // disabled by default
virtual bool getCADEnabled() const { return false; } // hardware CAD disabled by default
virtual int getAGCResetInterval() const { return 0; } // disabled by default
+3 -1
View File
@@ -11,6 +11,7 @@ Identity::Identity() {
}
Identity::Identity(const char* pub_hex) {
memset(pub_key, 0, sizeof(pub_key));
Utils::fromHex(pub_key, PUB_KEY_SIZE, pub_hex);
}
@@ -39,6 +40,7 @@ LocalIdentity::LocalIdentity() {
memset(prv_key, 0, sizeof(prv_key));
}
LocalIdentity::LocalIdentity(const char* prv_hex, const char* pub_hex) : Identity(pub_hex) {
memset(prv_key, 0, sizeof(prv_key));
Utils::fromHex(prv_key, PRV_KEY_SIZE, prv_hex);
}
@@ -140,4 +142,4 @@ void LocalIdentity::calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_k
ed25519_key_exchange(secret, other_pub_key, prv_key);
}
}
}
+60 -19
View File
@@ -165,6 +165,7 @@ void Mesh::begin() {
_flood_retries[i].retry_at = 0;
_flood_retries[i].retry_delay = 0;
_flood_retries[i].retry_attempts_sent = 0;
memset(_flood_retries[i].retry_key, 0, sizeof(_flood_retries[i].retry_key));
_flood_retries[i].priority = 0;
_flood_retries[i].progress_marker = 0;
_flood_retries[i].waiting_final_echo = false;
@@ -221,6 +222,18 @@ void Mesh::loop() {
if (_direct_retries[i].packet == getOutboundInFlight()) {
continue; // currently transmitting; keep slot until onSendComplete/onSendFail emits event
}
uint32_t elapsed_millis = _direct_retries[i].retry_started_at == 0
? 0
: (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at);
onDirectRetryEvent("dropped_queue_removed", NULL, 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].payload_type);
onDirectRetryEvent("failure", NULL, 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].payload_type);
onDirectRetryFailed(_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
clearDirectRetrySlot(i);
}
}
@@ -252,6 +265,13 @@ void Mesh::loop() {
if (_flood_retries[i].packet == getOutboundInFlight()) {
continue;
}
uint32_t elapsed_millis = _flood_retries[i].retry_started_at == 0
? 0
: (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at);
onFloodRetryEvent("dropped_queue_removed", NULL, elapsed_millis,
_flood_retries[i].retry_attempts_sent + 1);
onFloodRetryEvent("failure", NULL, elapsed_millis,
_flood_retries[i].retry_attempts_sent + 1);
clearFloodRetrySlot(i);
}
}
@@ -333,6 +353,17 @@ void Mesh::loop() {
#endif
}
bool Mesh::allowPacketTransmit(const Packet* packet) const {
#if defined(ENABLE_OTA)
if (packet != NULL && packet->getPayloadType() == PAYLOAD_TYPE_OTA) {
return isTempRadioActive();
}
#else
(void)packet;
#endif
return true;
}
bool Mesh::allowPacketForward(const mesh::Packet* packet) {
return false; // by default, Transport NOT enabled
}
@@ -1250,6 +1281,9 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) {
}
void Mesh::clearFloodRetrySlot(int idx) {
if (_flood_retries[idx].active) {
onFloodRetrySlotReleased(_flood_retries[idx].retry_key);
}
if (_flood_retries[idx].waiting_final_echo && _flood_retries[idx].packet != NULL) {
releasePacket(_flood_retries[idx].packet);
}
@@ -1259,6 +1293,7 @@ void Mesh::clearFloodRetrySlot(int idx) {
_flood_retries[idx].retry_at = 0;
_flood_retries[idx].retry_delay = 0;
_flood_retries[idx].retry_attempts_sent = 0;
memset(_flood_retries[idx].retry_key, 0, sizeof(_flood_retries[idx].retry_key));
_flood_retries[idx].priority = 0;
_flood_retries[idx].progress_marker = 0;
_flood_retries[idx].waiting_final_echo = false;
@@ -1342,16 +1377,9 @@ void Mesh::armFloodRetryOnSendComplete(const Packet* packet) {
max_attempts = FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX;
}
if (_flood_retries[i].retry_attempts_sent >= max_attempts) {
Packet* final_wait = obtainNewPacket();
if (final_wait == NULL) {
onFloodRetryEvent("dropped_no_packet", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent);
onFloodRetryEvent("failure", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent);
clearFloodRetrySlot(i);
continue;
}
*final_wait = *packet;
_flood_retries[i].packet = final_wait;
// Dispatcher releases the transmitted packet after this hook. Keep only
// retry metadata during the final echo window so RX retains the pool slot.
_flood_retries[i].packet = NULL;
_flood_retries[i].retry_at = futureMillis(_flood_retries[i].retry_delay);
_flood_retries[i].waiting_final_echo = true;
_flood_retries[i].queued = false;
@@ -1455,6 +1483,15 @@ void Mesh::maybeScheduleFloodRetry(const Packet* packet, uint8_t priority) {
return;
}
uint8_t retry_key[MAX_HASH_SIZE];
packet->calculatePacketHash(retry_key);
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
if (_flood_retries[i].active
&& memcmp(retry_key, _flood_retries[i].retry_key, MAX_HASH_SIZE) == 0) {
return; // the normal flood still sends, but only one retry sequence owns this logical packet
}
}
int slot_idx = -1;
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
if (!_flood_retries[i].active) {
@@ -1473,7 +1510,7 @@ void Mesh::maybeScheduleFloodRetry(const Packet* packet, uint8_t priority) {
}
uint32_t retry_delay = getFloodRetryAttemptDelay(packet, 0);
packet->calculatePacketHash(_flood_retries[slot_idx].retry_key);
memcpy(_flood_retries[slot_idx].retry_key, retry_key, sizeof(retry_key));
_flood_retries[slot_idx].packet = NULL;
_flood_retries[slot_idx].trigger_packet = const_cast<Packet*>(packet);
_flood_retries[slot_idx].retry_started_at = 0;
@@ -1755,14 +1792,16 @@ void Mesh::sendOtaFlood(Packet* packet, uint32_t delay_millis) {
}
#endif
void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_size) {
bool Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_size) {
if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime());
return;
releasePacket(packet);
return false;
}
if (path_hash_size == 0 || path_hash_size > 3) {
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): invalid path_hash_size", getLogDateTime());
return;
releasePacket(packet);
return false;
}
packet->header &= ~PH_ROUTE_MASK;
@@ -1780,17 +1819,19 @@ void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_si
pri = 1;
}
maybeScheduleFloodRetry(packet, pri);
sendPacket(packet, pri, delay_millis);
return sendPacket(packet, pri, delay_millis);
}
void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis, uint8_t path_hash_size) {
bool Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis, uint8_t path_hash_size) {
if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime());
return;
releasePacket(packet);
return false;
}
if (path_hash_size == 0 || path_hash_size > 3) {
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): invalid path_hash_size", getLogDateTime());
return;
releasePacket(packet);
return false;
}
packet->header &= ~PH_ROUTE_MASK;
@@ -1810,7 +1851,7 @@ void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_m
pri = 1;
}
maybeScheduleFloodRetry(packet, pri);
sendPacket(packet, pri, delay_millis);
return sendPacket(packet, pri, delay_millis);
}
void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis) {
+9 -2
View File
@@ -117,6 +117,7 @@ protected:
DispatcherAction onRecvPacket(Packet* pkt) override;
void onSendComplete(Packet* packet) override;
void onSendFail(Packet* packet) override;
bool allowPacketTransmit(const Packet* packet) const override;
virtual uint32_t getCADFailRetryDelay() const override;
@@ -215,9 +216,15 @@ protected:
/**
* \brief Optional hook for logging flood-retry lifecycle events.
* packet is null when the retained packet has already been released.
*/
virtual void onFloodRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { }
/**
* \brief Called exactly once whenever an active flood-retry slot is released.
*/
virtual void onFloodRetrySlotReleased(const uint8_t* retry_key) { }
/**
* \returns number of extra (Direct) ACK transmissions wanted.
*/
@@ -409,13 +416,13 @@ public:
/**
* \brief send a locally-generated Packet with flood routing
*/
void sendFlood(Packet* packet, uint32_t delay_millis=0, uint8_t path_hash_size=1);
bool sendFlood(Packet* packet, uint32_t delay_millis=0, uint8_t path_hash_size=1);
/**
* \brief send a locally-generated Packet with flood routing
* \param transport_codes array of 2 codes to attach to packet
*/
void sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0, uint8_t path_hash_size=1);
bool sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0, uint8_t path_hash_size=1);
/**
* \brief send a locally-generated Packet with Direct routing
+3 -1
View File
@@ -120,6 +120,7 @@ bool Utils::isHexChar(char c) {
}
bool Utils::fromHex(uint8_t* dest, int dest_size, const char *src_hex) {
if (dest == NULL || src_hex == NULL || dest_size < 0) return false;
int len = strlen(src_hex);
if (len != dest_size*2) return false; // incorrect length
@@ -127,6 +128,7 @@ bool Utils::fromHex(uint8_t* dest, int dest_size, const char *src_hex) {
while (dp - dest < dest_size) {
char ch = *src_hex++;
char cl = *src_hex++;
if (!isHexChar(ch) || !isHexChar(cl)) return false;
*dp++ = (hexVal(ch) << 4) | hexVal(cl);
}
return true;
@@ -150,4 +152,4 @@ int Utils::parseTextParts(char* text, const char* parts[], int max_num, char sep
return num;
}
}
}
+6 -8
View File
@@ -178,9 +178,9 @@ bool AlertReporter::sendChannel(const char* text) {
uint16_t codes[2];
codes[0] = scope.calcTransportCode(pkt);
codes[1] = 0;
_mesh->sendFlood(pkt, codes, 0, path_hash_size);
if (!_mesh->sendFlood(pkt, codes, 0, path_hash_size)) return false;
} else {
_mesh->sendFlood(pkt, 0, path_hash_size);
if (!_mesh->sendFlood(pkt, 0, path_hash_size)) return false;
}
ALERT_DEBUG_PRINTLN("sent: %s", text);
return true;
@@ -232,7 +232,7 @@ void AlertReporter::onLoop(unsigned long now_ms) {
if (_wifi.state == OK) {
if (wifi_down && down_ms >= thresh_ms &&
(now_ms - _wifi.fired_at_ms) >= min_interval_ms) {
(_wifi.fired_at_ms == 0 || (now_ms - _wifi.fired_at_ms) >= min_interval_ms)) {
char age[16];
formatAge(down_ms, age, sizeof(age));
uint8_t reason = MQTTBridge::getLastWifiDisconnectReason();
@@ -256,8 +256,7 @@ void AlertReporter::onLoop(unsigned long now_ms) {
formatAge(total, age, sizeof(age));
char text[80];
snprintf(text, sizeof(text), "WiFi recovered after %s", age);
sendChannel(text);
_wifi.state = OK;
if (sendChannel(text)) _wifi.state = OK;
}
}
} else if (_wifi.state == FIRING) {
@@ -282,7 +281,7 @@ void AlertReporter::onLoop(unsigned long now_ms) {
if (f.state == OK) {
if (down && down_ms >= thresh_ms &&
(now_ms - f.fired_at_ms) >= min_interval_ms) {
(f.fired_at_ms == 0 || (now_ms - f.fired_at_ms) >= min_interval_ms)) {
char age[16];
formatAge(down_ms, age, sizeof(age));
char text[100];
@@ -303,8 +302,7 @@ void AlertReporter::onLoop(unsigned long now_ms) {
char text[100];
snprintf(text, sizeof(text), "MQTT slot %d (%s) recovered after %s",
i + 1, _bridge->getSlotPresetName(i), age);
sendChannel(text);
f.state = OK;
if (sendChannel(text)) f.state = OK;
}
}
}
+6 -2
View File
@@ -98,6 +98,7 @@ bool ClientACL::clear() {
}
ClientInfo* ClientACL::getClient(const uint8_t* pubkey, int key_len) {
if (pubkey == NULL || key_len <= 0 || key_len > PUB_KEY_SIZE) return NULL;
for (int i = 0; i < num_clients; i++) {
if (memcmp(pubkey, clients[i].id.pub_key, key_len) == 0) return &clients[i]; // already known
}
@@ -106,7 +107,7 @@ ClientInfo* ClientACL::getClient(const uint8_t* pubkey, int key_len) {
ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
uint32_t min_time = 0xFFFFFFFF;
ClientInfo* oldest = &clients[MAX_CLIENTS - 1];
ClientInfo* oldest = NULL;
for (int i = 0; i < num_clients; i++) {
if (id.matches(clients[i].id)) return &clients[i]; // already known
if ( (!clients[i].isAdmin() && !clients[i].isRegionMgr())
@@ -120,6 +121,7 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
if (num_clients < MAX_CLIENTS) {
c = &clients[num_clients++];
} else {
if (oldest == NULL) return NULL; // every entry is protected
c = oldest; // evict least active contact
}
memset(c, 0, sizeof(*c));
@@ -131,6 +133,7 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
}
bool ClientACL::applyPermissions(const mesh::LocalIdentity& self_id, const uint8_t* pubkey, int key_len, uint8_t perms) {
if (pubkey == NULL || key_len <= 0 || key_len > PUB_KEY_SIZE) return false;
ClientInfo* c;
if ((perms & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { // guest role is not persisted in contacts
c = getClient(pubkey, key_len);
@@ -143,10 +146,11 @@ bool ClientACL::applyPermissions(const mesh::LocalIdentity& self_id, const uint8
i++;
}
} else {
if (key_len < PUB_KEY_SIZE) return false; // need complete pubkey when adding/modifying
if (key_len != PUB_KEY_SIZE) return false; // need complete pubkey when adding/modifying
mesh::Identity id(pubkey);
c = putClient(id, 0);
if (c == NULL) return false;
c->permissions = perms; // update their permissions
self_id.calcSharedSecret(c->shared_secret, pubkey);
+25 -9
View File
@@ -5,6 +5,7 @@
#include "AlertReporter.h" // for alertReporterBannedChannelMatch()
#include <RTClib.h>
#include <Utils.h>
#include <stddef.h>
#if defined(NRF52_PLATFORM)
#include <nrf.h>
@@ -1048,6 +1049,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_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;
memset(_prefs->battery_alert_region, 0, sizeof(_prefs->battery_alert_region));
_prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT;
_prefs->flood_channel_data_enabled = 1;
_prefs->flood_channel_block_max_hops = FLOOD_CHANNEL_BLOCK_HOPS_ALL;
@@ -1151,6 +1153,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->rx_ps_level, sizeof(_prefs->rx_ps_level));
file.read((uint8_t *)&_prefs->rx_ps_preamble, sizeof(_prefs->rx_ps_preamble));
}
if (file.available() >= (int)sizeof(_prefs->battery_alert_region)) {
file.read((uint8_t *)_prefs->battery_alert_region, sizeof(_prefs->battery_alert_region));
_prefs->battery_alert_region[sizeof(_prefs->battery_alert_region) - 1] = '\0';
}
}
// sanitise bad pref values
@@ -1369,7 +1375,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->rx_ps_sleep_us, sizeof(_prefs->rx_ps_sleep_us)); // 816
file.write((uint8_t *)&_prefs->rx_ps_level, sizeof(_prefs->rx_ps_level)); // 820
file.write((uint8_t *)&_prefs->rx_ps_preamble, sizeof(_prefs->rx_ps_preamble)); // 821
// next: 822
file.write((uint8_t *)_prefs->battery_alert_region, sizeof(_prefs->battery_alert_region)); // 822
// next: 853
file.close();
}
@@ -1422,12 +1429,15 @@ void CommonCLI::loadMQTTPrefs(FILESYSTEM* fs) {
// hadn't appended a tail field) leaves the trailing fields at their
// defaults, and a longer one (a future append) is truncated harmlessly.
size_t payload_avail = file_size - sizeof(hdr);
if (hdr.payload_len < payload_avail) payload_avail = hdr.payload_len;
size_t to_read = payload_avail < sizeof(_mqtt_prefs) ? payload_avail : sizeof(_mqtt_prefs);
size_t got = file.read((uint8_t *)&_mqtt_prefs, to_read);
if (got != to_read) {
setMQTTPrefsDefaults(&_mqtt_prefs);
} else {
has_observer_fields = true; // observer tail is part of the v1 payload
const size_t observer_tail_end = offsetof(MQTTPrefs, alert_region)
+ sizeof(_mqtt_prefs.alert_region);
has_observer_fields = to_read >= observer_tail_end;
}
} else {
// Unknown (newer) version: don't risk misreading a layout we don't know.
@@ -1775,9 +1785,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
} else if (memcmp(command, "neighbor.remove ", 16) == 0) {
const char* hex = &command[16];
uint8_t pubkey[PUB_KEY_SIZE];
int hex_len = min((int)strlen(hex), PUB_KEY_SIZE*2);
int pubkey_len = hex_len / 2;
if (mesh::Utils::fromHex(pubkey, pubkey_len, hex)) {
size_t hex_len = strlen(hex);
int pubkey_len = (int)(hex_len / 2);
if (hex_len > 0 && hex_len <= PUB_KEY_SIZE * 2 && (hex_len & 1) == 0
&& mesh::Utils::fromHex(pubkey, pubkey_len, hex)) {
_callbacks->removeNeighbor(pubkey, pubkey_len);
strcpy(reply, "OK");
} else {
@@ -2781,10 +2792,15 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "Error: channel must be between 1-14");
}
} else if (memcmp(config, "bridge.secret ", 14) == 0) {
StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret));
_callbacks->restartBridge();
savePrefs();
strcpy(reply, "OK");
const char* secret = &config[14];
if (secret[0] == 0 || strlen(secret) >= sizeof(_prefs->bridge_secret)) {
sprintf(reply, "Error: secret must be 1-%u characters", (unsigned)(sizeof(_prefs->bridge_secret) - 1));
} else {
StrHelper::strncpy(_prefs->bridge_secret, secret, sizeof(_prefs->bridge_secret));
_callbacks->restartBridge();
savePrefs();
strcpy(reply, "OK");
}
#endif
} else if (memcmp(config, "adc.multiplier ", 15) == 0) {
_prefs->adc_multiplier = atof(&config[15]);
+1
View File
@@ -214,6 +214,7 @@ struct NodePrefs { // persisted to file
uint32_t rx_ps_sleep_us;
uint8_t rx_ps_level; // 0 = manual/explicit us timings; 1..10 = level-derived (auto-retunes on SF/BW change)
uint8_t rx_ps_preamble; // 0 = auto (derive from SF); else 16 or 32 = explicit override for level calc
char battery_alert_region[31]; // named scope for low-battery floods; empty = no alert scope
};
#ifdef WITH_MQTT_BRIDGE
+90 -25
View File
@@ -79,36 +79,100 @@ bool RegionMap::load(FILESYSTEM* _fs, const char* path) {
if (file) {
uint8_t pad[128];
num_regions = 0; next_id = 1;
default_id = home_id = 0;
RegionEntry loaded[MAX_REGION_ENTRIES];
uint16_t loaded_count = 0;
uint16_t loaded_default = 0;
uint16_t loaded_home = 0;
uint16_t loaded_next = 1;
uint8_t loaded_wildcard_flags = 0;
bool success = file.read(pad, 3) == 3; // reserved header
success = success && file.read((uint8_t *) &default_id, sizeof(default_id)) == sizeof(default_id);
success = success && file.read((uint8_t *) &home_id, sizeof(home_id)) == sizeof(home_id);
success = success && file.read((uint8_t *) &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags);
success = success && file.read((uint8_t *) &next_id, sizeof(next_id)) == sizeof(next_id);
success = success && file.read((uint8_t *) &loaded_default, sizeof(loaded_default)) == sizeof(loaded_default);
success = success && file.read((uint8_t *) &loaded_home, sizeof(loaded_home)) == sizeof(loaded_home);
success = success && file.read((uint8_t *) &loaded_wildcard_flags, sizeof(loaded_wildcard_flags)) == sizeof(loaded_wildcard_flags);
success = success && file.read((uint8_t *) &loaded_next, sizeof(loaded_next)) == sizeof(loaded_next);
if (success) {
while (num_regions < MAX_REGION_ENTRIES) {
auto r = &regions[num_regions];
success = file.read((uint8_t *) &r->id, sizeof(r->id)) == sizeof(r->id);
success = success && file.read((uint8_t *) &r->parent, sizeof(r->parent)) == sizeof(r->parent);
success = success && file.read((uint8_t *) r->name, sizeof(r->name)) == sizeof(r->name);
success = success && file.read((uint8_t *) &r->flags, sizeof(r->flags)) == sizeof(r->flags);
success = success && file.read(pad, sizeof(pad)) == sizeof(pad);
if (!success) break; // EOF
if (r->id >= next_id) { // make sure next_id is valid
next_id = r->id + 1;
}
num_regions++;
while (success && file.available() > 0) {
if (loaded_count >= MAX_REGION_ENTRIES) {
success = false;
break;
}
RegionEntry& r = loaded[loaded_count];
memset(&r, 0, sizeof(r));
success = file.read((uint8_t *) &r.id, sizeof(r.id)) == sizeof(r.id);
success = success && file.read((uint8_t *) &r.parent, sizeof(r.parent)) == sizeof(r.parent);
success = success && file.read((uint8_t *) r.name, sizeof(r.name)) == sizeof(r.name);
success = success && file.read((uint8_t *) &r.flags, sizeof(r.flags)) == sizeof(r.flags);
success = success && file.read(pad, sizeof(pad)) == sizeof(pad);
if (!success) break;
const char* terminator = (const char*)memchr(r.name, 0, sizeof(r.name));
if (r.id == 0 || r.id == 0xFFFF || terminator == NULL || r.name[0] == 0) {
success = false;
break;
}
for (const char* p = r.name; *p; p++) {
if (!is_name_char((uint8_t)*p)) {
success = false;
break;
}
}
if (!success) break;
loaded_count++;
}
file.close();
return true;
// Validate IDs and parent links only after every record is present so forward
// parent references are allowed. A bounded walk also rejects cycles.
for (uint16_t i = 0; success && i < loaded_count; i++) {
for (uint16_t j = i + 1; j < loaded_count; j++) {
if (loaded[i].id == loaded[j].id) success = false;
}
uint16_t parent = loaded[i].parent;
for (uint16_t depth = 0; success && parent != 0; depth++) {
if (depth >= loaded_count || parent == loaded[i].id) {
success = false;
break;
}
const RegionEntry* found = NULL;
for (uint16_t j = 0; j < loaded_count; j++) {
if (loaded[j].id == parent) {
found = &loaded[j];
break;
}
}
if (found == NULL) {
success = false;
break;
}
parent = found->parent;
}
}
bool default_found = loaded_default == 0;
bool home_found = loaded_home == 0;
uint16_t minimum_next = 1;
for (uint16_t i = 0; success && i < loaded_count; i++) {
if (loaded[i].id == loaded_default) default_found = true;
if (loaded[i].id == loaded_home) home_found = true;
if (loaded[i].id >= minimum_next) minimum_next = loaded[i].id + 1;
}
if (!default_found || !home_found) success = false;
if (loaded_next == 0 || loaded_next == 0xFFFF || loaded_next < minimum_next) loaded_next = minimum_next;
if (loaded_next == 0xFFFF && loaded_count < MAX_REGION_ENTRIES) success = false;
if (success) {
memcpy(regions, loaded, loaded_count * sizeof(RegionEntry));
if (loaded_count < MAX_REGION_ENTRIES) {
memset(&regions[loaded_count], 0, (MAX_REGION_ENTRIES - loaded_count) * sizeof(RegionEntry));
}
num_regions = loaded_count;
default_id = loaded_default;
home_id = loaded_home;
next_id = loaded_next;
wildcard.flags = loaded_wildcard_flags;
}
return success;
}
}
return false; // failed
@@ -157,7 +221,8 @@ RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id, uint16_t
region->parent = parent_id; // re-parent / move this region in the hierarchy
} else {
if (id == 0 && num_regions >= MAX_REGION_ENTRIES) return NULL; // full!
if (num_regions >= MAX_REGION_ENTRIES) return NULL; // full!
if (id == 0xFFFF || (id == 0 && (next_id == 0 || next_id == 0xFFFF))) return NULL;
region = &regions[num_regions++]; // alloc new RegionEntry
region->flags = REGION_DENY_FLOOD; // DENY by default
+37 -12
View File
@@ -38,24 +38,34 @@ ESPNowBridge::ESPNowBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTC
void ESPNowBridge::begin() {
BRIDGE_DEBUG_PRINTLN("Initializing...\n");
if (_initialized) return;
// Initialize WiFi in station mode
WiFi.mode(WIFI_STA);
// Set Wi-Fi channel
if (esp_wifi_set_channel(_prefs->bridge_channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error setting WIFI channel to %d\n", _prefs->bridge_channel);
WiFi.mode(WIFI_OFF);
return;
}
// Initialize ESP-NOW
if (esp_now_init() != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error initializing ESP-NOW\n");
WiFi.mode(WIFI_OFF);
return;
}
// Register callbacks
esp_now_register_recv_cb(recv_cb);
esp_now_register_send_cb(send_cb);
if (esp_now_register_recv_cb(recv_cb) != ESP_OK || esp_now_register_send_cb(send_cb) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error registering ESP-NOW callbacks\n");
esp_now_register_recv_cb(nullptr);
esp_now_register_send_cb(nullptr);
esp_now_deinit();
WiFi.mode(WIFI_OFF);
return;
}
// Add broadcast peer
esp_now_peer_info_t peerInfo = {};
@@ -66,6 +76,10 @@ void ESPNowBridge::begin() {
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Failed to add broadcast peer\n");
esp_now_register_recv_cb(nullptr);
esp_now_register_send_cb(nullptr);
esp_now_deinit();
WiFi.mode(WIFI_OFF);
return;
}
@@ -102,11 +116,17 @@ void ESPNowBridge::loop() {
// Nothing to do here - ESP-NOW is callback based
}
void ESPNowBridge::xorCrypt(uint8_t *data, size_t len) {
size_t keyLen = strlen(_prefs->bridge_secret);
bool ESPNowBridge::xorCrypt(uint8_t *data, size_t len) {
size_t keyLen = 0;
while (keyLen < sizeof(_prefs->bridge_secret) && _prefs->bridge_secret[keyLen] != 0) keyLen++;
if (keyLen == 0 || keyLen == sizeof(_prefs->bridge_secret)) {
BRIDGE_DEBUG_PRINTLN("Invalid empty or unterminated bridge secret\n");
return false;
}
for (size_t i = 0; i < len; i++) {
data[i] ^= _prefs->bridge_secret[i % keyLen];
}
return true;
}
void ESPNowBridge::onDataRecv(const uint8_t *mac, const uint8_t *data, int len) {
@@ -135,7 +155,7 @@ void ESPNowBridge::onDataRecv(const uint8_t *mac, const uint8_t *data, int len)
memcpy(decrypted, data + BRIDGE_MAGIC_SIZE, encryptedDataLen);
// Try to decrypt (checksum + payload)
xorCrypt(decrypted, encryptedDataLen);
if (!xorCrypt(decrypted, encryptedDataLen)) return;
// Validate checksum
uint16_t received_checksum = (decrypted[0] << 8) | decrypted[1];
@@ -178,14 +198,19 @@ void ESPNowBridge::sendPacket(mesh::Packet *packet) {
if (!_seen_packets.wasSeen(packet)) {
_seen_packets.markSeen(packet);
// Create a temporary buffer just for size calculation and reuse for actual writing
// Check the serialized size before writing into the ESP-NOW-sized buffer.
const int expectedMeshPacketLen = packet->getRawLength();
if (expectedMeshPacketLen < 0 || (size_t)expectedMeshPacketLen > MAX_PAYLOAD_SIZE) {
BRIDGE_DEBUG_PRINTLN("TX packet too large (payload=%d, max=%d)\n", expectedMeshPacketLen,
MAX_PAYLOAD_SIZE);
return;
}
uint8_t sizingBuffer[MAX_PAYLOAD_SIZE];
uint16_t meshPacketLen = packet->writeTo(sizingBuffer);
// Check if packet fits within our maximum payload size
if (meshPacketLen > MAX_PAYLOAD_SIZE) {
BRIDGE_DEBUG_PRINTLN("TX packet too large (payload=%d, max=%d)\n", meshPacketLen,
MAX_PAYLOAD_SIZE);
if (meshPacketLen != (uint16_t)expectedMeshPacketLen) {
BRIDGE_DEBUG_PRINTLN("TX packet length mismatch (actual=%d, expected=%d)\n", meshPacketLen,
expectedMeshPacketLen);
return;
}
@@ -205,7 +230,7 @@ void ESPNowBridge::sendPacket(mesh::Packet *packet) {
buffer[3] = checksum & 0xFF; // Low byte
// Encrypt payload and checksum (not including magic header)
xorCrypt(buffer + BRIDGE_MAGIC_SIZE, meshPacketLen + BRIDGE_CHECKSUM_SIZE);
if (!xorCrypt(buffer + BRIDGE_MAGIC_SIZE, meshPacketLen + BRIDGE_CHECKSUM_SIZE)) return;
// Total packet size: magic header + checksum + payload
const size_t totalPacketSize = BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE + meshPacketLen;
+1 -1
View File
@@ -85,7 +85,7 @@ private:
* @param data Pointer to data to encrypt/decrypt
* @param len Length of data in bytes
*/
void xorCrypt(uint8_t *data, size_t len);
bool xorCrypt(uint8_t *data, size_t len);
/**
* ESP-NOW receive callback
+7 -1
View File
@@ -38,9 +38,15 @@ public:
* These two functions do nothing for ESP-NOW, but are needed for the
* Radio interface.
*/
virtual bool setRxBoostedGainMode(bool) { }
virtual bool setRxBoostedGainMode(bool) { return false; }
virtual bool getRxBoostedGainMode() const { return false; }
// ESP-NOW has no LoRa RX power-saving watchdog or noise-floor calibration.
uint32_t getRxPsWatchdogSoftCount() const { return 0; }
uint32_t getRxPsWatchdogHardCount() const { return 0; }
bool isWatchdogObserving() const { return false; }
bool isCalibratingNoiseFloor() const { return false; }
uint32_t intID();
void setTxPower(uint8_t dbm);
};
+17 -2
View File
@@ -434,6 +434,7 @@ void OtaManager::startFetch(const uint8_t* mid, uint32_t target, bool validate)
memcpy(_fid, mid, 4);
_fstate = WANT_MANIFEST;
_mf_total = 0; _mf_mask = 0; _mf_len = 0; _mf_retries = 0; _loop_last_mfmask = 0; // fresh manifest reassembly
memset(_mf_buf, 0, sizeof(_mf_buf));
GetManifestMsg gm; memcpy(gm.manifest_id, _fid, 4); gm.want_mask = 0xFFFF; // first ask: send all fragments
uint8_t b[16];
emit(b, encode_get_manifest(b, sizeof(b), gm), false);
@@ -445,10 +446,21 @@ void OtaManager::handleManifest(const uint8_t* m, uint16_t n) {
if (_fstate != WANT_MANIFEST || memcmp(mm.manifest_id, _fid, 4) != 0) return;
if (mm.frag_total == 0 || mm.frag_total > OTA_MF_MAXFRAG || mm.frag_idx >= mm.frag_total) return;
const uint8_t expected_total = (uint8_t)((MOTA_MFL + OTA_MF_FRAG - 1) / OTA_MF_FRAG);
const uint16_t expected_len = mm.frag_idx + 1 == expected_total
? (uint16_t)(MOTA_MFL - (uint32_t)mm.frag_idx * OTA_MF_FRAG)
: (uint16_t)OTA_MF_FRAG;
if (mm.frag_total != expected_total || mm.len != expected_len) return;
// reassemble the (possibly multi-fragment) manifest into _mf_buf; place fragment frag_idx at its offset
uint32_t foff = (uint32_t)mm.frag_idx * OTA_MF_FRAG;
if (foff + mm.len > sizeof(_mf_buf)) return;
if (mm.frag_total != _mf_total) { _mf_total = mm.frag_total; _mf_mask = 0; _mf_len = 0; } // (re)start
if (mm.frag_total != _mf_total) {
_mf_total = mm.frag_total;
_mf_mask = 0;
_mf_len = 0;
memset(_mf_buf, 0, sizeof(_mf_buf));
}
memcpy(_mf_buf + foff, mm.bytes, mm.len);
_mf_mask |= (uint16_t)(1u << mm.frag_idx);
if (mm.frag_idx == mm.frag_total - 1) _mf_len = foff + mm.len; // last fragment fixes the length
@@ -461,6 +473,7 @@ void OtaManager::handleManifest(const uint8_t* m, uint16_t n) {
if (!codecOk(mf[56])) { _fstate = IDLE; return; } // codec we can't apply (lying/stale ADV) — abort
uint32_t payload_size = rd_u32le(mf + 15);
uint8_t bsl = mf[19];
if (bsl >= 32) { _fstate = FAILED; return; }
uint32_t bs = 1u << bsl;
// a block must fit our reassembly buffer (and be non-empty) — reject an oversized block_size up front
if (bs == 0 || bs > OTA_MAX_BLOCK || payload_size == 0) { _fstate = FAILED; return; }
@@ -534,7 +547,9 @@ void OtaManager::handleLeaves(const uint8_t* m, uint16_t n) {
uint8_t ftotal = (uint8_t)((leaves_len + OTA_LEAVES_FRAG - 1) / OTA_LEAVES_FRAG); if (ftotal == 0) ftotal = 1;
if (lv.frag_total != ftotal || lv.frag_idx >= ftotal) return;
uint32_t foff = (uint32_t)lv.frag_idx * OTA_LEAVES_FRAG;
if (foff + lv.len > leaves_len) return; // slice out of range
uint32_t expected_len = leaves_len - foff;
if (expected_len > OTA_LEAVES_FRAG) expected_len = OTA_LEAVES_FRAG;
if (lv.len != expected_len) return; // reject short/oversized slices
if (lv.frag_total != _lv_total) { _lv_total = lv.frag_total; _lv_mask = 0; } // (re)start
memcpy(_leaves_buf + foff, lv.bytes, lv.len);
_lv_mask |= (uint16_t)(1u << lv.frag_idx);
+3 -4
View File
@@ -203,9 +203,8 @@ void OtaStoreFlashEsp32::checkpoint() {
bool OtaStoreFlashEsp32::reopen() {
if (!acquire() || _psize < SEC) return false;
uint8_t hb[8];
// the meta/container is staged at the BOTTOM of the slot, so scan upward from there; cap the scan (a
// miss just means "fetch fresh"). 128 sectors (512 KB) covers any full image's meta + typical deltas.
uint32_t scanned = 0;
// The meta/container is staged at the bottom of the slot. Large delta containers can begin well above
// the final 512 KB, so scan the complete partition rather than silently losing resumability after reboot.
for (uint32_t o = align_down(_psize - SEC, SEC); ; o -= SEC) {
if (esp_partition_read(_part, o, hb, 8) == ESP_OK && memcmp(hb, MOTA_MAGIC, 4) == 0) {
uint32_t total = rd_u32le(hb + 4);
@@ -232,7 +231,7 @@ bool OtaStoreFlashEsp32::reopen() {
}
}
}
if (o == 0 || ++scanned >= 128) break;
if (o == 0) break;
}
return false;
}
+59
View File
@@ -546,6 +546,65 @@ TEST(OtaTransfer, TwoManagersFullTransfer) {
EXPECT_TRUE(mota_check_image_hash_full(m));
}
static void deliver_manifest_fragment(OtaManager& client, const uint8_t mid[4], uint8_t frag_idx,
const uint8_t* bytes, uint16_t len) {
uint8_t wire[MAX_PACKET_PAYLOAD];
ManifestMsg msg;
memcpy(msg.manifest_id, mid, 4);
msg.frag_idx = frag_idx;
msg.frag_total = (uint8_t)((MOTA_MFL + OTA_MF_FRAG - 1) / OTA_MF_FRAG);
msg.bytes = bytes;
msg.len = len;
uint16_t wire_len = encode_manifest(wire, sizeof(wire), msg);
ASSERT_GT(wire_len, 0);
client.on_message(wire, wire_len);
}
TEST(OtaTransfer, RejectsShortNonFinalManifestFragment) {
g_q.clear();
MotaManifest manifest;
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
OtaManager client;
OtaStoreRam<4096> store;
SendTo to_none{&client};
client.begin(SIM_TARGET_ID, sim_send, &to_none);
client.set_fetch_store(&store);
client.pull(manifest.merkle_root, manifest.target_id);
g_q.clear();
const uint8_t* bytes = manifest.manifest_start;
const uint16_t final_len = (uint16_t)(MOTA_MFL - OTA_MF_FRAG);
deliver_manifest_fragment(client, manifest.merkle_root, 1, bytes + OTA_MF_FRAG, final_len);
deliver_manifest_fragment(client, manifest.merkle_root, 0, bytes, OTA_MF_FRAG - 1);
EXPECT_EQ(client.fetchState(), OtaManager::WANT_MANIFEST);
deliver_manifest_fragment(client, manifest.merkle_root, 0, bytes, OTA_MF_FRAG);
EXPECT_EQ(client.fetchState(), OtaManager::FETCHING);
}
TEST(OtaTransfer, RejectsManifestBlockExponentBeforeShift) {
g_q.clear();
MotaManifest manifest;
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
std::array<uint8_t, MOTA_MFL> bytes;
memcpy(bytes.data(), manifest.manifest_start, bytes.size());
bytes[19] = 32;
OtaManager client;
OtaStoreRam<4096> store;
SendTo to_none{&client};
client.begin(SIM_TARGET_ID, sim_send, &to_none);
client.set_fetch_store(&store);
client.pull(manifest.merkle_root, manifest.target_id);
g_q.clear();
deliver_manifest_fragment(client, manifest.merkle_root, 0, bytes.data(), OTA_MF_FRAG);
deliver_manifest_fragment(client, manifest.merkle_root, 1, bytes.data() + OTA_MF_FRAG,
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
EXPECT_EQ(client.fetchState(), OtaManager::FAILED);
}
// Same end-to-end transfer, but with 1 KB logical blocks: each block is delivered as several
// self-describing DATA fragments (frag_off), reassembled by the client, then its merkle PROOF is
// requested + verified separately before the block is committed. Exercises the multi-fragment path.
+22
View File
@@ -51,6 +51,28 @@ TEST(UtilsToHex, NullTerminatesOnEmptyInput) {
EXPECT_EQ('\0', output[0]);
}
TEST(UtilsFromHex, ConvertsUpperAndLowerCase) {
uint8_t output[3] = {};
ASSERT_TRUE(Utils::fromHex(output, sizeof(output), "0aBCfF"));
EXPECT_EQ(0x0A, output[0]);
EXPECT_EQ(0xBC, output[1]);
EXPECT_EQ(0xFF, output[2]);
}
TEST(UtilsFromHex, RejectsInvalidCharacters) {
uint8_t output[2] = {0xAA, 0xAA};
EXPECT_FALSE(Utils::fromHex(output, sizeof(output), "0G!1"));
}
TEST(UtilsFromHex, RejectsWrongLength) {
uint8_t output[2] = {};
EXPECT_FALSE(Utils::fromHex(output, sizeof(output), "001"));
EXPECT_FALSE(Utils::fromHex(output, sizeof(output), "001122"));
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
+2 -2
View File
@@ -150,7 +150,7 @@ extends = LilyGo_TLora_V2_1_1_6_core
extra_scripts =
${esp32_base.extra_scripts}
pre:scripts/generate_cert_bundle.py
board_ssl_cert_source = adafruit-full
board_ssl_cert_source = adafruit
board_build.embed_files = src/certs/x509_crt_bundle.bin
build_flags =
${LilyGo_TLora_V2_1_1_6_core.build_flags}
@@ -196,7 +196,7 @@ extends = LilyGo_TLora_V2_1_1_6_core
extra_scripts =
${esp32_base.extra_scripts}
pre:scripts/generate_cert_bundle.py
board_ssl_cert_source = adafruit-full
board_ssl_cert_source = adafruit
board_build.embed_files = src/certs/x509_crt_bundle.bin
build_flags =
${LilyGo_TLora_V2_1_1_6_core.build_flags}