Keep the Wi-Fi icon steady on OWE transition-mode networks (#14133)
* Keep the Wi-Fi icon steady on OWE transition-mode networks An OWE transition-mode network pairs an open SSID with a hidden "_owetm_" twin on the same BSSID. Between scans NetworkManager reports the in-use access point under the hidden SSID and drops the active profile from the device's AvailableConnections, which is the only place Quickshell builds known networks from. No listed network is then connected, so the bar fell back to the disconnected icon until the next scan brought the open SSID back, flickering on and off while the link stayed up. Fall back to the Wi-Fi device's own connected state when no listed network is connected, and read the in-use access point's strength from nmcli (without a rescan) while the connected network has none of its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Discard stale in-use AP reads and key Wi-Fi checks on the device Bump a generation whenever the cached in-use access point strength stops describing the link (leaving Wi-Fi or a device change), and drop any nmcli read started before it, re-reading immediately instead of a full interval later. Key Wi-Fi connectivity checks on the device rather than the SSID, so the listed network coming and going with each scan on an OWE transition-mode network no longer schedules a check. A real network switch still passes through "disconnected". Add a QML fixture that runs the panel against a mocked device through the scan churn, a mid-read disconnect, and a final disconnect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8e02fc84f5
commit
18328559b9
5 files changed
+289
-4
No files matched your search
@@ -14,6 +14,28 @@ function wifiIconFor(strength) {
|
||||
return icons[index]
|
||||
}
|
||||
|
||||
// The connected network's own strength comes from the access points
|
||||
// Quickshell grouped under its SSID. When it has none (an OWE transition-mode
|
||||
// network between scans files the in-use access point under its hidden
|
||||
// "_owetm_" twin), fall back to the strength NetworkManager reports for the
|
||||
// access point in use.
|
||||
function connectedSignalStrength(networkStrength, activeApSignal) {
|
||||
var strength = Math.round((networkStrength || 0) * 100)
|
||||
if (strength > 0) return strength
|
||||
return activeApSignal > 0 ? activeApSignal : strength
|
||||
}
|
||||
|
||||
// Reads `nmcli -t -f IN-USE,SIGNAL device wifi list` output, where the in-use
|
||||
// access point is the row marked "*".
|
||||
function parseActiveApSignal(raw) {
|
||||
var lines = String(raw || "").split(/\r?\n/)
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var match = /^\*:(\d+)$/.exec(lines[i].trim())
|
||||
if (match) return parseInt(match[1], 10)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// A known plain-HTTP endpoint lets the network redirect the browser to its
|
||||
// login page. Never execute or automatically open an untrusted Location header.
|
||||
var captivePortalUrl = "http://ping.archlinux.org/nm-check.txt"
|
||||
@@ -368,6 +390,8 @@ if (typeof module !== "undefined") {
|
||||
parseNetworkStatus: parseNetworkStatus,
|
||||
wifiIconFor: wifiIconFor,
|
||||
connectionIcon: connectionIcon,
|
||||
connectedSignalStrength: connectedSignalStrength,
|
||||
parseActiveApSignal: parseActiveApSignal,
|
||||
connectivityState: connectivityState,
|
||||
captivePortalUrl: captivePortalUrl,
|
||||
formatHeaderSpeed: formatHeaderSpeed,
|
||||
|
||||
@@ -388,6 +388,7 @@ Panel {
|
||||
onWifiDeviceChanged: {
|
||||
setScannerEnabled(true)
|
||||
syncWifiNetworks()
|
||||
resetActiveApSignal()
|
||||
}
|
||||
|
||||
onWifiNetworkObjectsChanged: syncWifiNetworks()
|
||||
@@ -441,11 +442,52 @@ Panel {
|
||||
readonly property string kind: {
|
||||
if (wiredDevice && wiredDevice.connected) return "ethernet"
|
||||
if (connectedWifiNetwork) return "wifi"
|
||||
// NetworkManager can leave the active profile out of the device's
|
||||
// AvailableConnections, and Quickshell only builds known networks from
|
||||
// that list. An OWE transition-mode network does this between scans: the
|
||||
// in-use access point carries the hidden "_owetm_" SSID, so the profile
|
||||
// for the open SSID looks out of range even while it is connected. Trust
|
||||
// the device's own state then instead of flickering to disconnected.
|
||||
if (wifiDevice && wifiDevice.connected) return "wifi"
|
||||
return "disconnected"
|
||||
}
|
||||
readonly property int signalStrength: connectedWifiNetwork
|
||||
? Math.round((connectedWifiNetwork.signalStrength || 0) * 100)
|
||||
readonly property int signalStrength: kind === "wifi"
|
||||
? Model.connectedSignalStrength(connectedWifiNetwork ? connectedWifiNetwork.signalStrength : 0, activeApSignal)
|
||||
: -1
|
||||
// Filled by activeApSignalPoll from nmcli while the connected network has no
|
||||
// strength of its own to report.
|
||||
property int activeApSignal: -1
|
||||
readonly property bool needsActiveApSignal: kind === "wifi"
|
||||
&& !(connectedWifiNetwork && connectedWifiNetwork.signalStrength > 0)
|
||||
// Bumped whenever the cached reading stops describing the current link, so
|
||||
// an nmcli read still in flight from before cannot write a stale strength.
|
||||
property int activeApSignalGeneration: 0
|
||||
property int activeApSignalRequest: -1
|
||||
onKindChanged: if (kind !== "wifi") resetActiveApSignal()
|
||||
|
||||
// Switching Wi-Fi networks always passes through a non-"wifi" kind (both the
|
||||
// device and the old network drop out of the connected state while the new
|
||||
// profile activates), so this also covers a change of network.
|
||||
function resetActiveApSignal() {
|
||||
activeApSignal = -1
|
||||
activeApSignalGeneration++
|
||||
}
|
||||
|
||||
function pollActiveApSignal() {
|
||||
if (activeApSignalProc.running) return
|
||||
activeApSignalRequest = activeApSignalGeneration
|
||||
activeApSignalProc.running = true
|
||||
}
|
||||
|
||||
function finishActiveApSignal(raw) {
|
||||
if (activeApSignalRequest !== activeApSignalGeneration) {
|
||||
// The link changed mid-read and the restarted poll skipped while this
|
||||
// one was running; read again now instead of a full interval later.
|
||||
if (needsActiveApSignal) activeApSignalPoll.restart()
|
||||
return
|
||||
}
|
||||
activeApSignal = Model.parseActiveApSignal(raw)
|
||||
}
|
||||
|
||||
function copyToClipboard(value) {
|
||||
if (!value || !root.bar) return
|
||||
@@ -464,8 +506,11 @@ Panel {
|
||||
readonly property bool hasCaptivePortal: connectivity === "portal"
|
||||
readonly property bool restricted: hasCaptivePortal || connectivity === "limited"
|
||||
readonly property string icon: Model.connectionIcon(kind, signalStrength, connectivity)
|
||||
readonly property string connectionKey: kind === "wifi" && wifiDevice && connectedWifiNetwork
|
||||
? kind + ":" + wifiDevice.name + ":" + connectedWifiNetwork.name
|
||||
// Keyed on the device rather than the SSID: on an OWE transition-mode
|
||||
// network the listed network comes and goes with every scan while the link
|
||||
// stays up, and a real network switch already passes through "disconnected".
|
||||
readonly property string connectionKey: kind === "wifi" && wifiDevice
|
||||
? kind + ":" + wifiDevice.name
|
||||
: (kind === "ethernet" && wiredDevice ? kind + ":" + wiredDevice.name : "")
|
||||
|
||||
onConnectionKeyChanged: Qt.callLater(checkConnectivity)
|
||||
@@ -871,6 +916,26 @@ Panel {
|
||||
}
|
||||
}
|
||||
|
||||
// Reads the in-use access point's strength without triggering a scan, for
|
||||
// the connected network that has none of its own (see signalStrength).
|
||||
Process {
|
||||
id: activeApSignalProc
|
||||
command: ["nmcli", "-t", "-f", "IN-USE,SIGNAL", "device", "wifi", "list", "ifname", root.wifiDevice ? root.wifiDevice.name : "", "--rescan", "no"]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: root.finishActiveApSignal(text)
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: activeApSignalPoll
|
||||
interval: 5000
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
running: root.needsActiveApSignal
|
||||
onTriggered: root.pollActiveApSignal()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: scanRestart
|
||||
interval: 100
|
||||
|
||||
Reference in new issue
Block a user