From a958afa419fa63a3c25ef86e9e8f4ab144de27ff Mon Sep 17 00:00:00 2001 From: rationalsa Date: Tue, 18 Jan 2022 19:35:43 +0000 Subject: [PATCH] Wifi: major rewrite for merging Too many changes to list here, see the PR comment: https://github.com/BELABOX/belaUI/pull/3#issuecomment-1007671335 --- belaUI.js | 707 +++++++++++++++++++++++++++++++++------------- public/index.html | 52 +++- public/script.js | 539 +++++++++++++++++++++-------------- public/style.css | 33 ++- 4 files changed, 899 insertions(+), 432 deletions(-) diff --git a/belaUI.js b/belaUI.js index 8970334..763da45 100644 --- a/belaUI.js +++ b/belaUI.js @@ -19,7 +19,7 @@ const http = require('http'); const finalhandler = require('finalhandler'); const serveStatic = require('serve-static'); const ws = require('ws'); -const { exec, execSync, spawn, spawnSync, execFileSync } = require("child_process"); +const { exec, execSync, spawn, spawnSync, execFileSync, execFile } = require("child_process"); const fs = require('fs') const crypto = require('crypto'); const path = require('path'); @@ -241,6 +241,7 @@ function getPipelineList() { /* Network interface list */ let netif = {}; + function updateNetif() { exec("ifconfig", (error, stdout, stderr) => { if (error) { @@ -251,15 +252,27 @@ function updateNetif() { let foundNewInt = false; const newints = {}; + wiFiDeviceListStartUpdate(); + const interfaces = stdout.split("\n\n"); for (const int of interfaces) { try { - const name = int.split(':')[0] + const name = int.split(':')[0]; + + let inetAddr = int.match(/inet (\d+\.\d+\.\d+\.\d+)/); + if (inetAddr) inetAddr = inetAddr[1]; + + // update the list of WiFi devices + if (name && name.match('^wlan')) { + let hwAddr = int.match(/ether ([0-9a-f:]+)/); + if (hwAddr) { + wiFiDeviceListAdd(name, hwAddr[1], inetAddr); + } + } + if (name == 'lo' || name.match('^docker') || name.match('^l4tbr')) continue; - let inetAddr = int.match(/inet \d+\.\d+\.\d+\.\d+/); - if (inetAddr == null) continue; - inetAddr = inetAddr[0].split(' ')[1] + if (!inetAddr) continue; let txBytes = int.match(/TX packets \d+ bytes \d+/); txBytes = parseInt(txBytes[0].split(' ').pop()); @@ -284,6 +297,12 @@ function updateNetif() { if (foundNewInt && isStreaming) { updateSrtlaIps(); } + + if (wiFiDeviceListEndUpdate()) { + console.log("updated wifi devices"); + // a delay seems to be needed before NM registers new devices + setTimeout(wifiUpdateDevices, 1000); + } }); } updateNetif(); @@ -317,273 +336,563 @@ function handleNetif(conn, msg) { conn.send(buildMsg('netif', netif)); } -/* Wifi */ -let wifiDeviceMACAddrs = {}; -// parses : separated values, with automatic \ escape detection and stripping -function parseNmcliSep(value) { - return value.split(/(? a.replace(/\\:/g, ':')); +/* + WiFi device list / status maintained by periodic ifconfig updates + + It tracks and detects changes by device name, physical (MAC) addresses and + IPv4 address. It allows us to only update the WiFi status via nmcli when + something has changed, because NM is very CPU / power intensive compared + to the periodic ifconfig polling that belaUI is already doing +*/ +let wifiDeviceHwAddr = {}; +let wiFiDeviceListIsModified = false; +let wiFiDeviceListIsUpdating = false; + +function wiFiDeviceListStartUpdate() { + if (wiFiDeviceListIsUpdating) { + throw "Called while an update was already in progress"; + } + + for (const i in wifiDeviceHwAddr) { + wifiDeviceHwAddr[i].removed = true; + } + wiFiDeviceListIsUpdating = true; + wiFiDeviceListIsModified = false +} + +function wiFiDeviceListAdd(ifname, hwAddr, inetAddr) { + if (!wiFiDeviceListIsUpdating) { + throw "Called without starting an update"; + } + + if (wifiDeviceHwAddr[ifname]) { + if (wifiDeviceHwAddr[ifname].hwAddr != hwAddr) { + wifiDeviceHwAddr[ifname].hwAddr = hwAddr; + wiFiDeviceListIsModified = true; + } + if (wifiDeviceHwAddr[ifname].inetAddr != inetAddr) { + wifiDeviceHwAddr[ifname].inetAddr = inetAddr; + wiFiDeviceListIsModified = true; + } + wifiDeviceHwAddr[ifname].removed = false; + } else { + wifiDeviceHwAddr[ifname] = { + hwAddr, + inetAddr + }; + wiFiDeviceListIsModified = true; + } +} + +function wiFiDeviceListEndUpdate() { + if (!wiFiDeviceListIsUpdating) { + throw "Called without starting an update"; + } + + for (const i in wifiDeviceHwAddr) { + if (wifiDeviceHwAddr[i].removed) { + delete wifiDeviceHwAddr[i]; + wiFiDeviceListIsModified = true; + } + } + + wiFiDeviceListIsUpdating = false; + return wiFiDeviceListIsModified; +} + +function wifiDeviceListGetAddr(ifname) { + if (wifiDeviceHwAddr[ifname]) { + return wifiDeviceHwAddr[ifname].hwAddr; + } } -function getKnownWifiConnections() { - let connections; + +/* NetworkManager / nmcli helpers */ +function nmConnsGet(fields) { try { - connections = execFileSync("nmcli", [ + const result = execFileSync("nmcli", [ "--terse", "--fields", - "uuid,type", + fields, "connection", "show", ]).toString("utf-8").split("\n"); - } catch (err) { - console.log(`Error getting the nmcli connection list: ${err.message}`); - return {}; + return result; + + } catch ({message}) { + console.log(`nmConnsGet err: ${message}`); } - const knownNetworks = {}; +} - for (const connection of connections) { - try { - const [uuid, type] = parseNmcliSep(connection); +function nmConnGetFields(uuid, fields) { + try { + const result = execFileSync("nmcli", [ + "--terse", + "--escape", "no", + "--get-values", + fields, + "connection", + "show", + uuid, + ]).toString("utf-8").split("\n"); + return result; - if (type !== "802-11-wireless") continue; + } catch ({message}) { + console.log(`nmConnGetFields err: ${message}`); + } +} - // Get the device the connection is bound to and the real ssid, since the connection name is prefixed. - const connectionInfo = execFileSync("nmcli", [ - "--terse", - "--escape", "no", - "--get-values", - "802-11-wireless.ssid,802-11-wireless.mac-address", - "connection", - "show", - uuid, - ]).toString("utf-8").split("\n"); - - const device = wifiDeviceMACAddrs[connectionInfo[1].toLowerCase()]; - const ssid = connectionInfo[0]; - - if (!device) continue; - - if (!knownNetworks[device]) knownNetworks[device] = []; - - knownNetworks[device].push({ - uuid, - ssid, - }); - } catch (err) { - console.log(`Error getting the nmcli connection information: ${err.message}`); +function nmConnDelete(uuid, callback) { + execFile("nmcli", ["conn", "del", uuid], function (error, stdout, stderr) { + let success = true; + if (error || !stdout.match("successfully deleted")) { + console.log(`nmConnDelete err: ${stdout}`); + success = false; } - } - return knownNetworks; + if (callback) { + callback(success); + } + }); +} + +function nmConnect(uuid, callback) { + execFile("nmcli", ["conn", "up", uuid], function (error, stdout, stderr) { + let success = true; + if (error || !stdout.match("^Connection successfully activated")) { + console.log(`nmConnect err: ${stdout}`); + success = false; + } + + if (callback) { + callback(success); + } + }); } -function getStatusWifiDevices() { - let networkDevices; +function nmDisconnect(uuid, callback) { + execFile("nmcli", ["conn", "down", uuid], function (error, stdout, stderr) { + let success = true; + if (error || !stdout.match("successfully deactivated")) { + console.log(`nmDisconnect err: ${stdout}`); + success = false; + } + + if (callback) { + callback(success); + } + }); +} + +function nmDevices(fields) { try { - networkDevices = execFileSync("nmcli", [ + const result = execFileSync("nmcli", [ "--terse", "--fields", - "type,device,state,con-uuid", + fields, "device", "status", ]).toString("utf-8").split("\n"); - } catch (err) { - console.log(`Error getting the nmcli device list: ${err.message}`); - return {}; - } - - const statusWifiDevices = {}; + return result; - for (const networkDevice of networkDevices) { - try { - const [type, device, state, uuid] = parseNmcliSep(networkDevice); - - if (type !== "wifi" || state == "unavailable") continue; + } catch ({message}) { + console.log(`nmDevices err: ${message}`); + } +} - const macAddr = execFileSync("nmcli", [ - "--terse", - "--escape", "no", - "--get-values", - "general.hwaddr", - "device", - "show", - device - ]).toString("utf-8").trim().toLowerCase(); - - wifiDeviceMACAddrs[macAddr] = device; - - statusWifiDevices[device] = { - state, - uuid, - ssid: "" - }; - - if (!uuid) continue; - - const ssid = execFileSync("nmcli", [ - "--terse", - "--escape", "no", - "--get-values", - "802-11-wireless.ssid", - "connection", - "show", - uuid, - ]).toString("utf-8").trim(); - - statusWifiDevices[device].ssid = ssid; - } catch (err) { - console.log(`Error getting the nmcli WiFi device information: ${err.message}`); - } +function nmRescan(device, callback) { + const args = ["device", "wifi", "rescan"]; + if (device) { + args.push("ifname"); + args.push(device); } + execFile("nmcli", args, function (error, stdout, stderr) { + let success = true; + if (error || stdout != "") { + console.log(`nmRescan err: ${stdout}`); + success = false; + } - return statusWifiDevices; + if (callback) { + callback(success); + } + }); } -function getAvailableWifiNetworks() { +function nmScanResults(fields) { try { - const wifiNetworks = execFileSync("nmcli", [ + const result = execFileSync("nmcli", [ "--terse", "--fields", - "active,ssid,signal,bars,security,freq,bssid,device", + fields, "device", "wifi", ]).toString("utf-8").split("\n"); + return result; + + } catch ({message}) { + console.log(`nmScanResults err: ${message}`); + } +} + +// parses : separated values, with automatic \ escape detection and stripping +function nmcliParseSep(value) { + return value.split(/(? a.replace(/\\:/g, ':')); +} + + +/* + NetworkManager / nmcli based Wifi Manager + + Structs: + + WiFi list : + { + 'mac': + } + + WiFi id to MAC address mapping : + { + id: 'mac' + } + + Wifi device : + { + 'id', // numeric id for the adapter - temporary for each belaUI execution + 'ifname': 'wlanX', + 'conn': 'uuid' or undefined; // the active connection + 'available': Map{}, + 'saved': {} + } + + Available network : + { + active, // is it currently connected? + ssid, + signal: 0-100, + security, + freq + } + + Saved networks {}: + { + ssid: uuid, + } +*/ +let wifiIfId = 0; +let wifiIfs = {}; +let wifiIdToHwAddr = {}; + +/* Builds the WiFi status structure sent over the network from the structures */ +function wifiBuildMsg() { + const ifs = {}; + for (const i in wifiIfs) { + const id = wifiIfs[i].id; + const s = wifiIfs[i]; + + ifs[id] = { + ifname: s.ifname, + conn: s.conn, + available: Array.from(s.available.values()), + saved: s.saved + }; + } + + return ifs; +} + +function wifiBroadcastState() { + broadcastMsg('status', {wifi: wifiBuildMsg()}); +} + + +function wifiUpdateSavedConns() { + let connections = nmConnsGet("uuid,type"); + if (connections === undefined) return; + + for (const i in wifiIfs) { + wifiIfs[i].saved = {}; + } - const sortedWifiNetworks = {}; + for (const connection of connections) { + try { + const [uuid, type] = nmcliParseSep(connection); - for (const wifiNetwork of wifiNetworks) { - const [active, ssid, signal, bars, security, freq, bssid, device] = - parseNmcliSep(wifiNetwork); + if (type !== "802-11-wireless") continue; - if (ssid == "" || ssid == null) continue; + // Get the device the connection is bound to and the ssid + const [ssid, macTmp] = nmConnGetFields(uuid, "802-11-wireless.ssid,802-11-wireless.mac-address"); - if (!sortedWifiNetworks[device]) sortedWifiNetworks[device] = []; + if (!ssid || !macTmp) continue; - sortedWifiNetworks[device].push({ - active: active === "yes" ? true : false, - ssid, - signal: parseInt(signal), - bars, - security, - freq: parseInt(freq), - bssid, - }); + const macAddr = macTmp.toLowerCase(); + if (wifiIfs[macAddr]) { + wifiIfs[macAddr].saved[ssid] = uuid; + } + } catch (err) { + console.log(`Error getting the nmcli connection information: ${err.message}`); } + } +} + +function wifiUpdateScanResult() { + const wifiNetworks = nmScanResults("active,ssid,signal,security,freq,device"); + if (!wifiNetworks) return; - return sortedWifiNetworks; - } catch ({ message }) { - console.log(message); - return {}; + for (const i in wifiIfs) { + wifiIfs[i].available = new Map(); } + + for (const wifiNetwork of wifiNetworks) { + const [active, ssid, signal, security, freq, device] = + nmcliParseSep(wifiNetwork); + + if (ssid == null || ssid == "") continue; + + const hwAddr = wifiDeviceListGetAddr(device); + if (!wifiIfs[hwAddr] || (active != 'yes' && wifiIfs[hwAddr].available.has(ssid))) continue; + + wifiIfs[hwAddr].available.set(ssid, { + active: (active == 'yes'), + ssid, + signal: parseInt(signal), + security, + freq: parseInt(freq), + }); + } + + wifiBroadcastState(); } -function refreshWifiNetworks() { - broadcastMsg("wifidevices", getStatusWifiDevices()); - broadcastMsg("wifinetworks", { - knownWifiConnections: getKnownWifiConnections(), - availableWifiNetworks: getAvailableWifiNetworks() - }); +/* + The WiFi scan results are updated some time after a rescan command is issued / + some time after a new WiFi adapter is plugged in. + This function sets up a number of timers to broadcast the updated scan results + with the expectation that eventually it will capture any relevant new results +*/ +function wifiScheduleScanUpdates() { + setTimeout(wifiUpdateScanResult, 1000); + setTimeout(wifiUpdateScanResult, 3000); + setTimeout(wifiUpdateScanResult, 5000); + setTimeout(wifiUpdateScanResult, 10000); } -function disconnectWifiDevice(device) { - try { - const disconnect = execFileSync("nmcli", [ - "device", - "disconnect", - device, - ]).toString("utf-8"); +function wifiUpdateDevices() { + let newDevices = false; + let statusChange = false; + + let networkDevices = nmDevices("device,type,state,con-uuid"); + if (!networkDevices) return; + + // sorts the results alphabetically by interface name + networkDevices.sort(); + + // mark all WiFi adapters as removed + for (const i in wifiIfs) { + wifiIfs[i].removed = true; + } + + // Rebuild the id-to-hwAddr map + wifiIdToHwAddr = {}; + + for (const networkDevice of networkDevices) { + try { + const [ifname, type, state, connUuid] = nmcliParseSep(networkDevice); + const conn = (connUuid != '') ? connUuid : null; + + if (type !== "wifi" || state == "unavailable") continue; - console.log("[Wifi]", disconnect); - } catch ({ message }) { - console.log("[Wifi]", message); + const hwAddr = wifiDeviceListGetAddr(ifname); + if (!hwAddr) continue; + + if (wifiIfs[hwAddr]) { + // the interface is still available + delete wifiIfs[hwAddr].removed; + + if (ifname != wifiIfs[hwAddr].ifname) { + wifiIfs[hwAddr].ifname = ifname; + statusChange = true; + } + if (conn != wifiIfs[hwAddr].conn) { + wifiIfs[hwAddr].conn = conn; + statusChange = true; + } + } else { + const id = wifiIfId++; + + wifiIfs[hwAddr] = { + id, + ifname, + conn, + available: new Map(), + saved: {} + }; + newDevices = true; + statusChange = true; + } + wifiIdToHwAddr[wifiIfs[hwAddr].id] = hwAddr; + } catch (err) { + console.log(`Error getting the nmcli WiFi device information: ${err.message}`); + } + } + + // delete removed adapters + for (const i in wifiIfs) { + if (wifiIfs[i].removed) { + delete wifiIfs[i]; + statusChange = true; + } + } + + if (newDevices) { + wifiUpdateSavedConns(); + wifiScheduleScanUpdates(); + } + if (statusChange) { + wifiUpdateScanResult(); + } + if (newDevices || statusChange) { + wifiBroadcastState(); } + console.log(wifiIfs); - refreshWifiNetworks(); + return statusChange; } -function deleteKnownConnection(uuid) { - try { - const deleteCon = execFileSync("nmcli", [ - "connection", - "delete", - "uuid", - uuid, - ]).toString("utf-8"); +function wifiRescan() { + nmRescan(undefined, function(success) { + /* A rescan request will fail if a previous one is in progress, + but we still attempt to update the results */ + wifiUpdateScanResult(); + wifiScheduleScanUpdates(); + }); +} - console.log("[Wifi]", deleteCon); - } catch ({ message }) { - console.log("[Wifi]", message); +/* Searches saved connections in wifiIfs by UUID */ +function wifiSearchConnection(uuid) { + let connFound; + for (const i in wifiIdToHwAddr) { + const macAddr = wifiIdToHwAddr[i]; + for (const s in wifiIfs[macAddr].saved) { + if (wifiIfs[macAddr].saved[s] == uuid) { + connFound = i; + break; + } + } } - refreshWifiNetworks(); + return connFound; } -function connectToNewNetwork(device, ssid, password) { +function wifiDisconnect(uuid) { + if (wifiSearchConnection(uuid) === undefined) return; + + nmDisconnect(uuid, function(success) { + if (success) { + wifiUpdateScanResult(); + wifiScheduleScanUpdates(); + } + }); +} + +function wifiForget(uuid) { + if (wifiSearchConnection(uuid) === undefined) return; + + nmConnDelete(uuid, function(success) { + if (success) { + wifiUpdateSavedConns(); + wifiUpdateScanResult(); + wifiScheduleScanUpdates(); + } + }); +} + +function wifiDeleteFailedConns() { + const connections = nmConnsGet("uuid,type,timestamp"); + for (const c in connections) { + const [uuid, type, ts] = nmcliParseSep(connections[c]); + if (type !== "802-11-wireless") continue; + if (ts == 0) { + nmConnDelete(uuid); + } + } +} + +function wifiNew(conn, msg) { + if (!msg.device || !msg.ssid) return; + if (!wifiIdToHwAddr[msg.device]) return; + + const device = wifiIfs[wifiIdToHwAddr[msg.device]].ifname; + const args = [ "-w", "15", "device", "wifi", "connect", - ssid, + msg.ssid, "ifname", device - ] + ]; - if (password) { + if (msg.password) { args.push('password'); - args.push(password); + args.push(msg.password); } - try { - const connect = execFileSync("nmcli", args).toString("utf-8"); + const senderId = conn.senderId; + execFile("nmcli", args, function(error, stdout, stderr) { + if (error || stdout.match('^Error:')) { + wifiDeleteFailedConns(); - console.log("[Wifi]", connect); - } catch ({ message }) { - console.log("[Wifi]", message); - } + if (stdout.match('Secrets were required, but not provided')) { + conn.send(buildMsg('wifi', {new: {error: "auth", device: msg.device}}, senderId)); + } else { + conn.send(buildMsg('wifi', {new: {error: "generic", device: msg.device}}, senderId)); + } + } else if (stdout.match('successfully activated')) { + wifiUpdateSavedConns(); + wifiUpdateScanResult(); - refreshWifiNetworks(); + conn.send(buildMsg('wifi', {new: {success: true, device: msg.device}}, senderId)); + } + }); } -function connectToKnownNetwork(uuid) { - try { - const connect = execFileSync("nmcli", [ - "connection", - "up", - uuid - ]).toString("utf-8"); +function wifiConnect(conn, uuid) { + const deviceId = wifiSearchConnection(uuid); + if (deviceId === undefined) return; - console.log("[Wifi]", connect); - } catch ({ message }) { - console.log("[Wifi]", message); - } + const senderId = conn.senderId; + nmConnect(uuid, function(success) { + wifiUpdateScanResult(); + conn.send(buildMsg('wifi', {connect: success, device: deviceId}, senderId)); + }); +} - refreshWifiNetworks(); +function handleWifi(conn, msg) { + for (const type in msg) { + switch(type) { + case 'connect': + wifiConnect(conn, msg[type]); + break; + case 'disconnect': + wifiDisconnect(msg[type]); + break; + case 'scan': + wifiRescan(); + break; + case 'new': + wifiNew(conn, msg[type]); + break; + case 'forget': + wifiForget(msg[type]); + break; + } + } } -function handleWifiCommand(conn, type) { - switch (type.command) { - case "connectToNewNetwork": - connectToNewNetwork(type.device, type.ssid, type.password); - break; - case "connectToOpenNetwork": - connectToNewNetwork(type.device, type.ssid); - break; - case "connectToKnownNetwork": - connectToKnownNetwork(type.uuid); - break; - case "refreshNetworks": - refreshWifiNetworks(); - break; - case "disconnectWifiDevice": - disconnectWifiDevice(type.device); - break; - case "deleteKnownConnection": - deleteKnownConnection(type.uuid) - break; - }; -}; /* Remote */ const remoteProtocolVersion = 4; @@ -1252,7 +1561,8 @@ function sendStatus(conn) { conn.send(buildMsg('status', {is_streaming: isStreaming, available_updates: availableUpdates, updating: softUpdateStatus, - ssh: getSshStatus(conn)})); + ssh: getSshStatus(conn), + wifi: wifiBuildMsg()})); } function sendInitialStatus(conn) { @@ -1262,7 +1572,6 @@ function sendInitialStatus(conn) { conn.send(buildMsg('netif', netif)); conn.send(buildMsg('sensors', sensors)); conn.send(buildMsg('revisions', revisions)); - conn.send(buildMsg('wifidevices', getStatusWifiDevices())); } function connAuth(conn, sendToken) { @@ -1347,8 +1656,8 @@ function handleMessage(conn, msg, isRemote = false) { case 'netif': handleNetif(conn, msg[type]); break; - case 'wifiCommand': - handleWifiCommand(conn, msg[type]); + case 'wifi': + handleWifi(conn, msg[type]); break; case 'logout': if (conn.authToken) { diff --git a/public/index.html b/public/index.html index b3a2c21..d6e28be 100644 --- a/public/index.html +++ b/public/index.html @@ -123,16 +123,54 @@ - - + + +
+ + + + + +
+ + + +
diff --git a/public/script.js b/public/script.js index fd46a39..f4e63a2 100644 --- a/public/script.js +++ b/public/script.js @@ -351,6 +351,10 @@ function updateStatus(status) { if (status.ssh) { showSshStatus(status.ssh); } + + if (status.wifi) { + updateWifiState(status.wifi); + } } @@ -404,253 +408,361 @@ function updateBitrate(br) { showBitrate(br.max_br); } -/* Wifi Settings */ -const wifiElement = document.querySelector("#wifi"); -// Function to request a refresh of the wifi networks -function refreshWifiNetworks() { +/* WiFi manager */ +function wifiScan(button, deviceId) { if (!ws) return; - ws.send(JSON.stringify({ - wifiCommand: { - command: "refreshNetworks", + + // Disable the search button immediately + const wifiManager = $(button).parents('.wifi-settings'); + wifiManager.find('.wifi-scan-button').attr('disabled', true); + + // Send the request + ws.send(JSON.stringify({wifi: {scan: deviceId}})); + + // Duration + const searchDuration = 10000; + + setTimeout(function() { + wifiManager.find('.wifi-scan-button').attr('disabled', false); + wifiManager.find('.scanning').addClass('d-none'); + }, searchDuration); + + wifiManager.find('.connect-error').addClass('d-none'); + wifiManager.find('.scanning').removeClass('d-none'); +} + +function wifiSendNewConnection() { + $('#wifiNewErrAuth').addClass('d-none'); + $('#wifiNewErrGeneric').addClass('d-none'); + $('#wifiNewConnecting').removeClass('d-none'); + + $('#wifiConnectButton').attr('disabled', true); + + const device = $('#connection-device').val(); + const ssid = $('#connection-ssid').val(); + const password = $('#connection-password').val(); + + ws.send(JSON.stringify({ + wifi: { + new: { + device, + ssid, + password + } } })); - // Disable buttons and add a loading spinner - document.querySelectorAll(".refreshbutton").forEach((button) => button.disabled = true); - document.querySelectorAll(".networks").forEach((network) => network.innerHTML = ` - - -
-
- Loading... -
-
- - - `); - document.querySelectorAll(".knownNetworks").forEach((network) => network.innerHTML = ""); -}; - -function connectToNetworkHandler(dataset) { - if (dataset.uuid) { - ws.send(JSON.stringify({ - wifiCommand: { - command: "connectToKnownNetwork", - uuid: dataset.uuid + return false; +} + +function wifiConnect(e) { + const network = $(e).parents('tr.network').data('network'); + + if (network.active) return; + + if (network.uuid) { + ws.send(JSON.stringify({wifi: {connect: network.uuid}})); + + const wifiManager = $(e).parents('.wifi-settings'); + wifiManager.find('.connect-error').addClass('d-none'); + wifiManager.find('.connecting').removeClass('d-none'); + } else { + if (network.security === "") { + if (confirm(`Connect to the open network ${network.ssid}?`)) { + ws.send(JSON.stringify({ + wifi: { + new: { + ssid: network.ssid, + device: network.device + } + } + })); + } + } else { + if (network.security.match('802.1X')) { + alert("This network uses 802.1X enterprise authentication, " + + "which belaUI doesn't support at the moment"); + } else if (network.security.match('WEP')) { + alert("This network uses legacy WEP authentication, " + + "which belaUI doesn't support"); + } else { + $('#connection-ssid').val(network.ssid); + $('#connection-device').val(network.device); + $('#connection-password').val(''); + $('.wifi-new-status').addClass('d-none'); + $('#wifiConnectButton').attr('disabled', false); + $('#wifiModal').modal({ show: true }); + + setTimeout(() => { + $('#connection-password').focus(); + }, 500); + } + } + } +} + +function wifiDisconnect(e) { + const network = $(e).parents('tr').data('network'); + + if (confirm(`Disconnect from ${network.ssid}?`)) { + ws.send(JSON.stringify({ + wifi: { + disconnect: network.uuid }, })); - } else if (dataset.security === "") { - ws.send(JSON.stringify({ - wifiCommand: { - command: "connectToOpenNetwork", - ssid: dataset.ssid, - device: dataset.device + } +} + +function wifiForget(e) { + const network = $(e).parents('tr').data('network'); + + if (confirm(`Forget network ${network.ssid}?`)) { + ws.send(JSON.stringify({ + wifi: { + forget: network.uuid }, })); - } else { - $('#wifiModal').find('#wifiModalTitle').text("Connect to network"); - $('#wifiModal').find('#wifiModalBody').html(` -
-
- - -
-
- - -
-
- `); - $('#wifiModal').find('#wifiModalFooter').html(` - - - `); - - $('#wifiModal').modal({ show: true }); - setTimeout(() => { - $('#connection-password').focus(); - }, 500); - } -} - -function connectToNetworkWithPW(device, ssid) { - const password = $('#connection-password').val(); + } +} - ws.send(JSON.stringify({ - wifiCommand: { - command: "connectToNewNetwork", - device, - ssid, - password - }, - })); +function wifiFindCardId(deviceId) { + return `wifi-manager-${parseInt(deviceId)}`; } -function deleteKnownConnectionHandler(dataset) { - $('#wifiModal').find('#wifiModalTitle').text("Delete connection?"); - $('#wifiModal').find('#wifiModalBody').html(` -
-
- - -
-
- `); - $('#wifiModal').find('#wifiModalFooter').html(` - - - `); - - $('#wifiModal').modal({ show: true }); -} - -function deleteKnownConnection(uuid) { - ws.send(JSON.stringify({ - wifiCommand: { - command: "deleteKnownConnection", - uuid - }, - })); +function wifiSignalSymbol(signal) { + if (signal < 0) signal = 0; + if (signal > 100) signal = 100; + const symbol = 9601 + Math.floor(signal / 12.51); + let cl = "text-success"; + if (signal < 40) { + cl = "text-danger"; + } else if (signal < 75) { + cl = "text-warning"; + } + return `&#${symbol}`; } -function disconnectWifiDevice(device) { - ws.send(JSON.stringify({ - wifiCommand: { - command: "disconnectWifiDevice", - device - }, - })); +function wifiListAvailableNetwork(device, deviceId, a) { + const savedUuid = device.saved[a.ssid]; + if (savedUuid) { + delete device.saved[a.ssid]; + } + + const html = ` + + + + + + Connected
+ + + + + + + `; + + const network = $($.parseHTML(html)); + network.find('.signal').html(wifiSignalSymbol(a.signal));// + '%'); + network.find('.band').html((a.freq > 5000) ? '5㎓' : '2.4㎓'); + const ssidEl = network.find('.ssid'); + ssidEl.text(a.ssid); + + network.data('network', {active: a.active, uuid: savedUuid, ssid: a.ssid, device: deviceId, security: a.security}); + + if (a.security != '') { + // show a cross mark for 802.1X or WEP networks (unsupported) + // or a lock symbol for PSK networks (supported) + network.find('.security').html(a.security.match(/802\.1X|WEP/) ? '❌' : '🔒'); + } + if (a.active) { + network.find('.disconnect').removeClass('d-none'); + network.find('.connected').removeClass('d-none'); + } + if (!a.active) { + network.find('.ssid').addClass('can-connect'); + } + if (savedUuid) { + network.find('.forget').removeClass('d-none'); + } + + return network; +} + +function wifiListSavedNetwork(ssid, uuid) { + const html = ` + + + + + + `; + + const network = $($.parseHTML(html)); + network.find('.ssid').text(ssid); + + network.data('network', {ssid, uuid}); + + return network; } -// Update wifi devices based on new status -function updateWifiDevices(status) { - const devices = Object.keys(status); +let wifiIfs = {}; +function updateWifiState(msg) { + for (const i in wifiIfs) { + wifiIfs[i].removed = true; + } - devices.forEach((device) => { - // If wifi device is not yet on the page, add it. - if (document.querySelector(`#${device}`) == null) { + for (let deviceId in msg) { + deviceId = parseInt(deviceId); + + // Mark the interface as not removed + if (wifiIfs[deviceId]) { + delete wifiIfs[deviceId].removed; + } + + const cardId = wifiFindCardId(deviceId); + const device = msg[deviceId]; + let deviceCard = $(`#${cardId}`); + + if (deviceCard.length == 0) { const html = ` -
-
-
-
+
+ - -
- -
+
+
+
+ Connecting...
-
- - +
+ Error connecting to the network. Has the password changed? +
-
+
+
+
+ Scanning... +
- - +
+
- - +
+ + + +
Other saved networks
-
-
- ` - - wifiElement.insertAdjacentHTML('beforeend', html); - }; - - // Set values for current connection - document.getElementById(`connection-${device}`).value = status[device].ssid ? status[device].ssid : "----"; - - // Add / Remove buttons for current connection - const conButtons = document.getElementById(`conButtons-${device}`); - conButtons.innerHTML = status[device].ssid ? ` - - - ` : ""; - }); +
`; - // Cleanup disconnected devices from UI - const wifiSettingsElements = document.querySelectorAll(".wifi-settings"); - wifiSettingsElements.forEach((we) => { - if (devices.includes(we.id)) return; - we.remove(); - }); -} + deviceCard = $($.parseHTML(html)); -function updateWifiNetworks({ knownWifiConnections, availableWifiNetworks }) { - Object.keys(availableWifiNetworks).forEach(device => { - const wifiNetworksFiltered = availableWifiNetworks[device].filter((n) => n.active !== true).map((n) => { - // If known connection, add a delete button - const knownConnection = knownWifiConnections[device] && knownWifiConnections[device].find((c) => c.ssid === n.ssid) || null; + deviceCard.appendTo('#wifi'); + } - const html = ` - - ${n.security != "" ? ` - - - ` : ""} - - ${n.bars} - ${n.ssid} - ${knownConnection ? ` - - ` : ""} - - - `; + // Update the card's header + deviceCard.find('.device-name').text(device.ifname); - return $($.parseHTML(html)); - }); + // Show the available networks + let networkList = []; - // Remove known networks if they are visible in scanlist - const knownNetworksFiltered = knownWifiConnections[device] && knownWifiConnections[device].filter((n) => !availableWifiNetworks[device].find((c) => c.ssid === n.ssid)).map((n) => { - const html = ` - - ${n.ssid} - - - - - `; - - return $($.parseHTML(html)); - }); - - $(`#wifiNetworks-${device}`).html(wifiNetworksFiltered); - $(`#knownWifiNetworks-${device}`).html(knownNetworksFiltered); - - document.querySelectorAll(".refreshbutton").forEach((button) => button.disabled = false); - document.querySelectorAll(".lastRefresh").forEach((el) => el.innerHTML = `Last update: ${new Date().toLocaleTimeString()}`); - }); + for (const a of msg[deviceId].available) { + if (a.active) { + networkList.push(wifiListAvailableNetwork(device, deviceId, a)); + } + } + + for (const a of msg[deviceId].available) { + if (!a.active) { + networkList.push(wifiListAvailableNetwork(device, deviceId, a)); + } + } + + deviceCard.find('.available-networks').html(networkList); + + // Show the saved networks + networkList = []; + for (const ssid in msg[deviceId].saved) { + const uuid = msg[deviceId].saved[ssid]; + networkList.push(wifiListSavedNetwork(ssid, uuid)); + } + + if (networkList.length) { + deviceCard.find('tbody.saved-networks').html(networkList); + deviceCard.find('table.saved-networks').removeClass('d-none'); + } else { + deviceCard.find('table.saved-networks').addClass('d-none'); + } + } + + for (const i in wifiIfs) { + if (wifiIfs[i].removed) { + const cardId = wifiFindCardId(i); + $(`#${cardId}`).remove(); + } + } + + wifiIfs = msg; +} + +function handleWifiResult(msg) { + if (msg.connect !== undefined) { + const wifiManagerId = `#${wifiFindCardId(msg.device)}`; + $(wifiManagerId).find('.connecting').addClass('d-none'); + if (msg.connect === false) { + $(wifiManagerId).find('.connect-error').removeClass('d-none'); + } + } else if (msg.new) { + if (msg.new.error) { + $('#wifiNewConnecting').addClass('d-none'); + + switch (msg.new.error) { + case 'auth': + $('#wifiNewErrAuth').removeClass('d-none'); + break; + case 'generic': + $('#wifiNewErrGeneric').removeClass('d-none'); + break; + } + + $('#wifiConnectButton').attr('disabled', false); + } + if (msg.new.success) { + $('#wifiModal').modal('hide'); + } + } } @@ -694,11 +806,8 @@ function handleMessage(msg) { case 'bitrate': updateBitrate(msg[type]); break; - case 'wifidevices': - updateWifiDevices(msg[type]) - break; - case 'wifinetworks': - updateWifiNetworks(msg[type]) + case 'wifi': + handleWifiResult(msg[type]); break; case 'error': showError(msg[type].msg); diff --git a/public/style.css b/public/style.css index 4b178af..4939908 100644 --- a/public/style.css +++ b/public/style.css @@ -23,26 +23,37 @@ max-width: 400px; } -.networks>tr:hover { - background: rgb(240, 240, 240); +td.signal { + width: 20px; +} + +td.band { + width: 40px; + font-family: monospace; + font-size: 12px; } td.security { text-align: right; - width: 3%; + width: 20px; } -td.ssid:hover { +.can-connect { cursor: pointer; } -td.signal { - font-family: monospace; - width: 5%; - font-size: 14px; +.networks td { vertical-align: bottom; } -td.deleteButton { - padding: 0.3rem; -} \ No newline at end of file +@media screen and (max-width: 500px) { + .button-text { + display: none; + } +} + +@media screen and (min-width: 500px) { + .button-icon { + display: none; + } +}