Files
blomarchy/bin/blob-network-band
T

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