372 lines
12 KiB
Bash
372 lines
12 KiB
Bash
#!/bin/sh
|
||
# wifiscan24.sh — 2.4 GHz channel scout for ath1 (BusyBox-safe)
|
||
# Default: probe 1/6/11 (+ current), print recommendation, restore original.
|
||
#
|
||
# Flags:
|
||
# --goforit : scan + apply best, policy prefers 1/6/11, verify
|
||
# --force : as above, but pre-reset AP (disable/enable) before applying
|
||
# --allbymyself : try like --goforit; if apply fails, auto-bounce AP and retry once
|
||
# --keepbadchannels : allow 2–5 or 7–10 if they win (only matters with goforit/force/allbymyself)
|
||
# --chan <N> : override — switch directly to channel N (ignores scan & policy)
|
||
# works alone (no reset) or with --force (preflight reset)
|
||
# --deep : deeper scan (longer dwell); works with apply modes
|
||
#
|
||
# Score: score = 7*OBSSavg + 3*CUavg (lower is better)
|
||
# Ties: lower OBSSmax → lower (more negative) noise → lower CUavg
|
||
#
|
||
# Apply policy:
|
||
# By default apply only to {1,6,11}. To allow middle channels {2..5,7..10} add --keepbadchannels
|
||
sleep 20
|
||
|
||
IFACE="${IFACE:-ath1}"
|
||
CHANNELS="${CHANNELS:-1 6 11}"
|
||
CSA_BEACONS="${CSA_BEACONS:-10}" # CSA duration (~1s at default beacon interval)
|
||
WAIT_AFTER_SWITCH="${WAIT_AFTER_SWITCH:-2}" # settle time after CSA during scan/apply
|
||
SAMPLES="${SAMPLES:-5}" # samples per channel
|
||
SLEEP_BETWEEN="${SLEEP_BETWEEN:-1}" # seconds between samples
|
||
|
||
# Deep-scan presets (used only with --deep)
|
||
DEEP_WAIT="2"
|
||
DEEP_SAMPLES="10"
|
||
DEEP_SLEEP="1"
|
||
|
||
GOOD_CH_SET="1 6 11" # policy-preferred set
|
||
|
||
GOFORIT=0
|
||
FORCE=0
|
||
ALLBY=0
|
||
DEEP=0
|
||
KEEPBAD=0
|
||
OVERRIDE_CH=""
|
||
|
||
# ---- arg parse ----
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
--goforit|--go|--apply) GOFORIT=1 ;;
|
||
--force) FORCE=1 ;;
|
||
--allbymyself) ALLBY=1 ;;
|
||
--deep) DEEP=1 ;;
|
||
--keepbadchannels) KEEPBAD=1 ;;
|
||
--chan)
|
||
shift
|
||
[ -n "$1" ] || { echo "Error: --chan requires a channel number." >&2; exit 1; }
|
||
OVERRIDE_CH="$1"
|
||
;;
|
||
-h|--help)
|
||
cat <<EOF
|
||
Usage: $0 [--goforit|--force|--allbymyself] [--keepbadchannels] [--deep] [--chan N]
|
||
|
||
Apply modes (choose one):
|
||
--goforit scan + apply best (policy prefers 1/6/11), verify
|
||
--force as above, but pre-reset AP before applying
|
||
--allbymyself like --goforit; if apply fails, auto-bounce AP and retry once
|
||
|
||
Extras:
|
||
--keepbadchannels allow 2–5/7–10 if they win (only with apply modes)
|
||
--deep deeper scan dwell (more stable numbers)
|
||
--chan N override: switch immediately to channel N (ignores scan & policy)
|
||
EOF
|
||
exit 0
|
||
;;
|
||
*)
|
||
echo "Unknown option: $1" >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
shift
|
||
done
|
||
|
||
# If multiple apply flags passed, pick strongest precedence: FORCE > ALLBY > GOFORIT
|
||
if [ "$FORCE" -eq 1 ]; then
|
||
GOFORIT=0
|
||
ALLBY=0
|
||
elif [ "$ALLBY" -eq 1 ]; then
|
||
GOFORIT=0
|
||
fi
|
||
|
||
# Deep dwell
|
||
if [ "$DEEP" -eq 1 ]; then
|
||
WAIT_AFTER_SWITCH="$DEEP_WAIT"
|
||
SAMPLES="$DEEP_SAMPLES"
|
||
SLEEP_BETWEEN="$DEEP_SLEEP"
|
||
fi
|
||
|
||
# ---- helpers ----
|
||
freq_from_ch() { ch="$1"; [ "$ch" -eq 14 ] && { echo 2484; return; }; echo $((2412 + 5*(ch-1))); }
|
||
ch_from_freq() { f="$1"; [ "$f" -eq 2484 ] && { echo 14; return; }; echo $(( (f-2412)/5 + 1 )); }
|
||
|
||
valid_ch() {
|
||
case "$1" in
|
||
''|*[!0-9]*) return 1 ;;
|
||
*) [ "$1" -ge 1 ] && [ "$1" -le 14 ] ;;
|
||
esac
|
||
}
|
||
|
||
in_good_set() {
|
||
case " $GOOD_CH_SET " in
|
||
*" $1 "*) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
get_current_freq() {
|
||
f="$(hostapd_cli -i "$IFACE" status 2>/dev/null | awk -F= '/^freq=/{print $2; exit}')"
|
||
[ -n "$f" ] || f="$(wifitool "$IFACE" get_rrmutil 2>/dev/null | awk -F': *' '/^channel/{print $2; exit}')"
|
||
echo "$f"
|
||
}
|
||
|
||
measure_once() {
|
||
# prints: "chan_util obss_util noise"
|
||
wifitool "$IFACE" get_rrmutil 2>/dev/null | awk -F': *' '
|
||
/^chan util/ {cu=$2+0}
|
||
/^obss util/ {ob=$2+0}
|
||
/^noise floor/ {nf=$2+0}
|
||
END {printf "%d %d %d\n", cu, ob, nf}
|
||
'
|
||
}
|
||
|
||
# CSA wrapper (quiet)
|
||
do_csa() {
|
||
hostapd_cli -i "$IFACE" chan_switch "$CSA_BEACONS" "$1" ht bandwidth=20 blocktx >/dev/null 2>&1
|
||
}
|
||
|
||
# Verify helper
|
||
verify_freq() {
|
||
target="$1"
|
||
sleep "$WAIT_AFTER_SWITCH"
|
||
cur="$(get_current_freq)"
|
||
[ "$cur" = "$target" ]
|
||
}
|
||
|
||
# Self-bounce helper
|
||
ap_bounce() {
|
||
hostapd_cli -i "$IFACE" disable >/dev/null 2>&1
|
||
sleep 2
|
||
hostapd_cli -i "$IFACE" enable >/dev/null 2>&1
|
||
sleep 2
|
||
}
|
||
|
||
# ---- prereqs ----
|
||
command -v hostapd_cli >/dev/null 2>&1 || { echo "hostapd_cli not found"; exit 1; }
|
||
command -v wifitool >/dev/null 2>&1 || { echo "wifitool not found"; exit 1; }
|
||
|
||
orig_freq="$(get_current_freq)"; orig_ch="$(ch_from_freq "$orig_freq")"
|
||
|
||
# ---------- OVERRIDE PATH: --chan N ----------
|
||
if [ -n "$OVERRIDE_CH" ]; then
|
||
valid_ch "$OVERRIDE_CH" || { echo "Invalid channel: $OVERRIDE_CH (expected 1..14)"; exit 1; }
|
||
target_freq="$(freq_from_ch "$OVERRIDE_CH")"
|
||
if [ "$target_freq" = "$orig_freq" ]; then
|
||
echo "Already on channel $OVERRIDE_CH ($target_freq MHz). No switch needed."
|
||
exit 0
|
||
fi
|
||
|
||
if [ "$FORCE" -eq 1 ]; then
|
||
echo "Pre-resetting AP before override…"
|
||
ap_bounce
|
||
fi
|
||
|
||
echo "Applying override: hostapd_cli -i $IFACE chan_switch $CSA_BEACONS $target_freq ht bandwidth=20 blocktx"
|
||
do_csa "$target_freq"
|
||
if verify_freq "$target_freq"; then
|
||
echo "Switched to ch $OVERRIDE_CH ($target_freq MHz)."
|
||
exit 0
|
||
else
|
||
echo "Override failed to take effect. Try: --chan $OVERRIDE_CH --force"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
# ---------- SCAN PATH ----------
|
||
# unique set incl. current
|
||
unique=""; add(){ case " $unique " in *" $1 "*) : ;; *) unique="$unique $1";; esac; }
|
||
for c in $CHANNELS; do add "$c"; done; add "$orig_ch"
|
||
|
||
echo "Interface: $IFACE"
|
||
echo "Original: ch $orig_ch ($orig_freq MHz)"
|
||
[ "$DEEP" -eq 1 ] && echo "Mode: deep scan (WAIT=$WAIT_AFTER_SWITCH s, SAMPLES=$SAMPLES, SLEEP=$SLEEP_BETWEEN s)"
|
||
echo "Testing: $unique"
|
||
echo
|
||
|
||
tmp="$(mktemp)"
|
||
RESTORE=1
|
||
cleanup() {
|
||
if [ "$RESTORE" = 1 ]; then
|
||
do_csa "$orig_freq"
|
||
fi
|
||
rm -f "$tmp"
|
||
}
|
||
trap 'cleanup' EXIT INT TERM
|
||
|
||
measure_channel() {
|
||
ch="$1"; freq="$(freq_from_ch "$ch")"
|
||
curf="$(get_current_freq)"
|
||
if [ "$curf" != "$freq" ]; then
|
||
do_csa "$freq"
|
||
sleep "$WAIT_AFTER_SWITCH"
|
||
fi
|
||
|
||
cu_min=9999; cu_max=0; cu_sum=0
|
||
ob_min=9999; ob_max=0; ob_sum=0
|
||
nf_min=9999; nf_max=-9999; nf_sum=0
|
||
|
||
i=0
|
||
while [ "$i" -lt "$SAMPLES" ]; do
|
||
set -- $(measure_once) # $1=cu $2=ob $3=nf
|
||
cu="$1"; ob="$2"; nf="$3"
|
||
|
||
[ "$cu" -lt "$cu_min" ] && cu_min="$cu"; [ "$cu" -gt "$cu_max" ] && cu_max="$cu"; cu_sum=$((cu_sum+cu))
|
||
[ "$ob" -lt "$ob_min" ] && ob_min="$ob"; [ "$ob" -gt "$ob_max" ] && ob_max="$ob"; ob_sum=$((ob_sum+ob))
|
||
[ "$nf" -lt "$nf_min" ] && nf_min="$nf"; [ "$nf" -gt "$nf_max" ] && nf_max="$nf"; nf_sum=$((nf_sum+nf))
|
||
|
||
i=$((i+1))
|
||
[ "$i" -lt "$SAMPLES" ] && sleep "$SLEEP_BETWEEN"
|
||
done
|
||
|
||
cu_avg=$((cu_sum / SAMPLES)); ob_avg=$((ob_sum / SAMPLES)); nf_avg=$((nf_sum / SAMPLES))
|
||
|
||
printf "ch %2d (%4d MHz): OBSS min/avg/max=%3d/%3d/%3d CHutil min/avg/max=%3d/%3d/%3d Noise avg=%4d dBm\n" \
|
||
"$ch" "$freq" "$ob_min" "$ob_avg" "$ob_max" "$cu_min" "$cu_avg" "$cu_max" "$nf_avg"
|
||
|
||
# machine line: ch ob_avg cu_avg ob_max nf_avg
|
||
echo "__RESULT__ $ch $ob_avg $cu_avg $ob_max $nf_avg"
|
||
}
|
||
|
||
# Measure all
|
||
for ch in $unique; do
|
||
out="$(measure_channel "$ch")"
|
||
printf "%s\n" "$out" | head -n 1
|
||
printf "%s\n" "$out" | awk '/^__RESULT__/ {print}' >>"$tmp"
|
||
done
|
||
|
||
# Pick best overall and best among 1/6/11
|
||
best_any_ch=""; best_any_score=999999; best_any_obmax=9999; best_any_nf=0; best_any_cu=9999
|
||
best_good_ch=""; best_good_score=999999; best_good_obmax=9999; best_good_nf=0; best_good_cu=9999
|
||
|
||
while read -r _ ch ob cu obmax nf; do
|
||
score=$(( 7*ob + 3*cu ))
|
||
|
||
# overall best
|
||
if [ "$score" -lt "$best_any_score" ]; then
|
||
best_any_score="$score"; best_any_ch="$ch"; best_any_obmax="$obmax"; best_any_nf="$nf"; best_any_cu="$cu"
|
||
elif [ "$score" -eq "$best_any_score" ]; then
|
||
# Tie-breaker without command groups — ash-safe
|
||
if [ "$obmax" -lt "$best_any_obmax" ]; then
|
||
best_any_ch="$ch"; best_any_obmax="$obmax"; best_any_nf="$nf"; best_any_cu="$cu"
|
||
elif [ "$obmax" -eq "$best_any_obmax" ]; then
|
||
if [ "$nf" -lt "$best_any_nf" ]; then
|
||
best_any_ch="$ch"; best_any_obmax="$obmax"; best_any_nf="$nf"; best_any_cu="$cu"
|
||
elif [ "$nf" -eq "$best_any_nf" ] && [ "$cu" -lt "$best_any_cu" ]; then
|
||
best_any_ch="$ch"; best_any_obmax="$obmax"; best_any_nf="$nf"; best_any_cu="$cu"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# best among 1/6/11
|
||
if in_good_set "$ch"; then
|
||
if [ "$score" -lt "$best_good_score" ]; then
|
||
best_good_score="$score"; best_good_ch="$ch"; best_good_obmax="$obmax"; best_good_nf="$nf"; best_good_cu="$cu"
|
||
elif [ "$score" -eq "$best_good_score" ]; then
|
||
# Tie-breaker without command groups — ash-safe
|
||
if [ "$obmax" -lt "$best_good_obmax" ]; then
|
||
best_good_ch="$ch"; best_good_obmax="$obmax"; best_good_nf="$nf"; best_good_cu="$cu"
|
||
elif [ "$obmax" -eq "$best_good_obmax" ]; then
|
||
if [ "$nf" -lt "$best_good_nf" ]; then
|
||
best_good_ch="$ch"; best_good_obmax="$obmax"; best_good_nf="$nf"; best_good_cu="$cu"
|
||
elif [ "$nf" -eq "$best_good_nf" ] && [ "$cu" -lt "$best_good_cu" ]; then
|
||
best_good_ch="$ch"; best_good_obmax="$obmax"; best_good_nf="$nf"; best_good_cu="$cu"
|
||
fi
|
||
fi
|
||
fi
|
||
fi
|
||
done <"$tmp"
|
||
|
||
best_any_freq="$(freq_from_ch "$best_any_ch")"
|
||
best_good_freq=""; [ -n "$best_good_ch" ] && best_good_freq="$(freq_from_ch "$best_good_ch")"
|
||
|
||
echo
|
||
echo ">>> Best by score: ch $best_any_ch ($best_any_freq MHz) [score=${best_any_score} = 7*OBSSavg + 3*CUavg]"
|
||
|
||
# Apply target by policy
|
||
apply_ch=""; apply_freq=""; note="policy prefers 1/6/11"
|
||
if [ "$KEEPBAD" -eq 1 ]; then
|
||
apply_ch="$best_any_ch"; apply_freq="$best_any_freq"; note="--keepbadchannels set (allow 2–5/7–10)"
|
||
else
|
||
if [ -n "$best_good_ch" ]; then
|
||
apply_ch="$best_good_ch"; apply_freq="$best_good_freq"
|
||
else
|
||
apply_ch="$best_any_ch"; apply_freq="$best_any_freq"; note="no scorable 1/6/11; falling back to overall best"
|
||
fi
|
||
fi
|
||
|
||
echo ">>> Policy target (apply): ch $apply_ch ($apply_freq MHz) [$note]"
|
||
[ "$apply_freq" = "$orig_freq" ] && APPLY_SAME=1 || APPLY_SAME=0
|
||
|
||
# ---------- APPLY SECTION ----------
|
||
if [ "$GOFORIT" -eq 1 ] || [ "$FORCE" -eq 1 ] || [ "$ALLBY" -eq 1 ]; then
|
||
if [ "$APPLY_SAME" -eq 1 ]; then
|
||
echo "Already on the policy target channel. No switch needed."
|
||
RESTORE=0
|
||
exit 0
|
||
fi
|
||
fi
|
||
|
||
# Common apply attempt
|
||
try_apply_once() {
|
||
tgt="$1"
|
||
echo "Applying (no reset): hostapd_cli -i $IFACE chan_switch $CSA_BEACONS $tgt ht bandwidth=20 blocktx"
|
||
do_csa "$tgt"
|
||
if verify_freq "$tgt"; then
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
if [ "$FORCE" -eq 1 ]; then
|
||
echo "Pre-resetting AP (force)…"
|
||
ap_bounce
|
||
echo "Applying after reset: hostapd_cli -i $IFACE chan_switch $CSA_BEACONS $apply_freq ht bandwidth=20 blocktx"
|
||
do_csa "$apply_freq"
|
||
if verify_freq "$apply_freq"; then
|
||
echo "Switched to ch $apply_ch ($apply_freq MHz) after AP reset."
|
||
RESTORE=0
|
||
exit 0
|
||
else
|
||
echo "Unable to switch automatically at this time. Keeping the original channel."
|
||
exit 1
|
||
fi
|
||
elif [ "$ALLBY" -eq 1 ]; then
|
||
# First: normal CSA
|
||
if try_apply_once "$apply_freq"; then
|
||
echo "Switched to ch $apply_ch ($apply_freq MHz)."
|
||
RESTORE=0
|
||
exit 0
|
||
fi
|
||
# Second: bounce then retry once
|
||
echo "Apply failed; self-bouncing AP and retrying once…"
|
||
ap_bounce
|
||
if try_apply_once "$apply_freq"; then
|
||
echo "Switched to ch $apply_ch ($apply_freq MHz) after self-bounce."
|
||
RESTORE=0
|
||
exit 0
|
||
fi
|
||
echo "Retry after self-bounce also failed. Keeping the original channel."
|
||
exit 1
|
||
elif [ "$GOFORIT" -eq 1 ]; then
|
||
if try_apply_once "$apply_freq"; then
|
||
echo "Switched to ch $apply_ch ($apply_freq MHz)."
|
||
RESTORE=0
|
||
exit 0
|
||
fi
|
||
echo "Cannot switch now; the radio needs a brief reset. Re-run with --allbymyself or --force${KEEPBAD:+ --keepbadchannels}."
|
||
exit 1
|
||
fi
|
||
|
||
# ---------- SCAN-ONLY ----------
|
||
echo "Scan-only mode."
|
||
echo "To apply (prefer 1/6/11): $0 --goforit"
|
||
echo "To self-heal on failure: $0 --allbymyself"
|
||
echo "To apply with reset: $0 --force"
|
||
echo "To allow middle chans: add --keepbadchannels with one of the above"
|
||
echo "To override to a chan: $0 --chan N (optionally add --force)"
|