193 lines
5.4 KiB
Bash
Executable File
193 lines
5.4 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# blob:summary=Show or pin the Wi-Fi band for the active connection
|
|
# blob:group=network
|
|
# blob:args=[auto|2.4|5|6]
|
|
# blob:examples=blob network band | blob network band 5 | blob network band auto
|
|
|
|
set -euo pipefail
|
|
|
|
# NetworkManager 1.44+ accepts a third band value, so 5GHz and 6GHz can be
|
|
# pinned apart. Pinning the band rather than a BSSID keeps working when the AP
|
|
# rotates BSSIDs, and leaves roaming between APs intact.
|
|
nm_band_for() {
|
|
case "$1" in
|
|
2.4) echo "bg" ;;
|
|
5) echo "a" ;;
|
|
6) echo "6GHz" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
band_from_nm() {
|
|
case "$1" in
|
|
bg) echo "2.4" ;;
|
|
a) echo "5" ;;
|
|
6GHz) echo "6" ;;
|
|
*) echo "auto" ;;
|
|
esac
|
|
}
|
|
|
|
# Accepts either nmcli's "2412 MHz" or iw's "5745.0": keep the leading digits
|
|
# and ignore whatever unit or fraction follows. The boundaries mirror Model.js
|
|
# formatHeaderFreq so the panel's label and this command cannot disagree.
|
|
band_for_freq() {
|
|
local mhz=${1%%[!0-9]*}
|
|
[[ -n $mhz ]] || return 1
|
|
|
|
if ((mhz >= 2400 && mhz < 2500)); then
|
|
echo "2.4"
|
|
elif ((mhz >= 4900 && mhz < 5925)); then
|
|
echo "5"
|
|
elif ((mhz >= 5925 && mhz < 7125)); then
|
|
echo "6"
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Every value read back from nmcli goes through here. LC_ALL=C because nmcli
|
|
# translates state words such as "connected", which would silently stop matching
|
|
# under a non-English session; `-e no` because `-g` otherwise escapes ':' and
|
|
# '\' in values, and an escaped SSID would never match iw's raw one.
|
|
nm_get() {
|
|
LC_ALL=C nmcli -e no -g "$@" 2>/dev/null
|
|
}
|
|
|
|
wifi_device() {
|
|
nm_get DEVICE,TYPE,STATE device status |
|
|
awk -F: '$2 == "wifi" && $3 == "connected" { print $1; exit }'
|
|
}
|
|
|
|
wifi_profile() {
|
|
nm_get GENERAL.CONNECTION device show "$1"
|
|
}
|
|
|
|
selected_band() {
|
|
band_from_nm "$(nm_get 802-11-wireless.band connection show "$1")"
|
|
}
|
|
|
|
# Sets $ssid and $freq from one `iw dev <device> link`. iw prints the SSID as
|
|
# the rest of its line, so an SSID containing ':' needs no special handling.
|
|
read_link() {
|
|
local link
|
|
link=$(iw dev "$1" link 2>/dev/null)
|
|
ssid=$(awk '/SSID:/ { sub(/.*SSID: /, ""); print; exit }' <<<"$link")
|
|
freq=$(awk '/freq:/ { print $2; exit }' <<<"$link")
|
|
}
|
|
|
|
# Every band the SSID is reachable on, low to high, always including the one
|
|
# already in use -- a weak radio gets missed by plenty of scans, and the band we
|
|
# are sitting on must never be absent from its own list of options. A band the
|
|
# AP does not answer on is never offered: pinning to it would drop the
|
|
# connection with nothing to reassociate to.
|
|
#
|
|
# --rescan no reads NetworkManager's cache, which the panel's own scanner keeps
|
|
# warm; forcing a scan here would stall every poll. The kernel's own cache (iw
|
|
# scan dump) is pruned far harder and would make bands flicker.
|
|
available_bands() {
|
|
local device=$1 ssid=$2 current=$3
|
|
|
|
{
|
|
if [[ -n $current ]]; then echo "$current"; fi
|
|
|
|
# SSID is queried last so one containing ':' can be reassembled verbatim.
|
|
# It reaches awk through the environment, not -v, which would expand
|
|
# backslash escapes in an SSID that contains one.
|
|
nm_get FREQ,SSID dev wifi list ifname "$device" --rescan no |
|
|
want="$ssid" awk -F: '
|
|
BEGIN { want = ENVIRON["want"] }
|
|
{
|
|
name = $2
|
|
for (i = 3; i <= NF; i++) name = name ":" $i
|
|
if (name == want) print $1
|
|
}' |
|
|
while read -r freq; do band_for_freq "$freq" || true; done
|
|
} | sort -u -g | tr '\n' ' ' | sed 's/ $//'
|
|
}
|
|
|
|
print_status() {
|
|
local device profile band available
|
|
|
|
device=$(wifi_device)
|
|
[[ -n $device ]] || return 0
|
|
|
|
read_link "$device"
|
|
[[ -n $ssid ]] || return 0
|
|
|
|
band=$(band_for_freq "$freq" || true)
|
|
available=$(available_bands "$device" "$ssid" "$band")
|
|
profile=$(wifi_profile "$device")
|
|
|
|
printf 'band\t%s\n' "$band"
|
|
printf 'available\t%s\n' "$available"
|
|
if [[ -n $profile ]]; then printf 'selected\t%s\n' "$(selected_band "$profile")"; fi
|
|
}
|
|
|
|
set_band() {
|
|
local target=$1
|
|
local device profile previous desired
|
|
|
|
device=$(wifi_device)
|
|
if [[ -z $device ]]; then
|
|
echo "Error: no connected Wi-Fi device." >&2
|
|
exit 1
|
|
fi
|
|
|
|
profile=$(wifi_profile "$device")
|
|
if [[ -z $profile ]]; then
|
|
echo "Error: no active Wi-Fi connection profile." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ $target == "auto" ]]; then
|
|
desired=""
|
|
else
|
|
read_link "$device"
|
|
if [[ " $(available_bands "$device" "$ssid" "$(band_for_freq "$freq" || true)") " != *" $target "* ]]; then
|
|
echo "Error: ${target}GHz is not available on this network." >&2
|
|
exit 1
|
|
fi
|
|
desired=$(nm_band_for "$target")
|
|
fi
|
|
|
|
previous=$(nm_get 802-11-wireless.band connection show "$profile")
|
|
[[ $previous == "$desired" ]] && exit 0
|
|
|
|
nmcli connection modify "$profile" 802-11-wireless.band "$desired" >/dev/null
|
|
|
|
# A band change only takes effect on reassociation. If the radio cannot come
|
|
# back up on the requested band, put the previous setting back and reconnect
|
|
# rather than leaving the machine stranded offline.
|
|
if ! nmcli connection up "$profile" >/dev/null 2>&1; then
|
|
nmcli connection modify "$profile" 802-11-wireless.band "$previous" >/dev/null
|
|
nmcli connection up "$profile" >/dev/null 2>&1 || true
|
|
echo "Error: could not connect on ${target}; reverted to previous band." >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
usage() {
|
|
echo "Usage: blob-network-band [auto|2.4|5|6]" >&2
|
|
}
|
|
|
|
if (($# == 0)); then
|
|
print_status
|
|
exit 0
|
|
fi
|
|
|
|
if (($# > 1)); then
|
|
usage
|
|
exit 1
|
|
fi
|
|
|
|
case "$1" in
|
|
auto | 2.4 | 5 | 6)
|
|
set_band "$1"
|
|
;;
|
|
*)
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|