// network.js — pure iwd D-Bus (busctl --json) shaping. NO I/O. // Dual-mode: node `require` (tests) + QML import ("network.js" as Network). // Same pattern as audio.js / brightness.js / clip.js. No `.pragma library`. // // iwd exposes a full D-Bus API (net.connman.iwd). `busctl --json=short` emits // structured JSON, so there is NO text/ANSI parsing — we JSON.parse two calls: // ObjectManager.GetManagedObjects -> devices, stations, networks, known nets // Station.GetOrderedNetworks -> [path, signalStrength] strongest-first // Network names arrive already decoded; signal is int dBm*100 (e.g. -6800). // --- busctl JSON helpers --------------------------------------------------- // busctl --json wraps a reply as { "type": "...", "data": [ ] }. // Unwrap to the single payload value. Accepts a raw string or a parsed object. function unwrap(raw) { if (raw === undefined || raw === null) return null; var obj = typeof raw === "string" ? JSON.parse(raw) : raw; if (obj && Object.prototype.hasOwnProperty.call(obj, "data") && Array.isArray(obj.data)) { return obj.data.length > 0 ? obj.data[0] : null; } return obj; } // A D-Bus property value is { "type": "x", "data": }. Pull data or fb. function prop(iface, name, fb) { if (iface && iface[name] && iface[name].data !== undefined) return iface[name].data; return fb; } // --- Signal ----------------------------------------------------------------- // signalBars(centiDbm) — iwd reports signal as dBm*100. Map to 0..4 bars using // the same thresholds iwctl itself renders. Non-finite => 0. function signalBars(centiDbm) { var v = Number(centiDbm); if (!isFinite(v)) return 0; if (v >= -6000) return 4; if (v >= -6700) return 3; if (v >= -7400) return 2; return 1; } // dbmFromCenti(centiDbm) — iwd's dBm*100 -> rounded dBm. Non-finite => null. function dbmFromCenti(centiDbm) { var v = Number(centiDbm); if (!isFinite(v)) return null; return Math.round(v / 100); } // --- Parsers (JSON -> plain data) ------------------------------------------ // parseManaged(rawGmo) -> { // devicePath, device:{name,powered,mode}|null, // station:{state,scanning,connectedNetwork}|null, // networks: { : {ssid,security,connected,known} }, // knownBySsid: { : } } // Picks the FIRST station-mode device. function parseManaged(rawGmo) { var d = unwrap(rawGmo) || {}; var out = { devicePath: "", device: null, station: null, networks: {}, knownBySsid: {} }; for (var path in d) { if (!Object.prototype.hasOwnProperty.call(d, path)) continue; var ifaces = d[path]; var dev = ifaces["net.connman.iwd.Device"]; if (dev && prop(dev, "Mode", "") === "station" && out.devicePath === "") { out.devicePath = path; out.device = { name: prop(dev, "Name", ""), powered: !!prop(dev, "Powered", false), mode: "station" }; var st = ifaces["net.connman.iwd.Station"]; if (st) { out.station = { state: prop(st, "State", ""), scanning: !!prop(st, "Scanning", false), connectedNetwork: prop(st, "ConnectedNetwork", "") }; } } var net = ifaces["net.connman.iwd.Network"]; if (net) { out.networks[path] = { ssid: prop(net, "Name", ""), security: prop(net, "Type", ""), connected: !!prop(net, "Connected", false), // iwd exposes a KnownNetwork property ONLY when the net is known. known: Object.prototype.hasOwnProperty.call(net, "KnownNetwork") }; } var kn = ifaces["net.connman.iwd.KnownNetwork"]; if (kn) { var kssid = prop(kn, "Name", ""); if (kssid !== "") out.knownBySsid[kssid] = path; } } return out; } // parseOrdered(rawGon) -> [{ path, signal:centiDbm }] in iwd order (the API // already lists the connected network first, then by descending strength). function parseOrdered(rawGon) { var arr = unwrap(rawGon); var out = []; if (!Array.isArray(arr)) return out; for (var i = 0; i < arr.length; i++) { var pair = arr[i]; if (!Array.isArray(pair) || pair.length < 2) continue; out.push({ path: pair[0], signal: pair[1] }); } return out; } // networkRows(managed, ordered) -> view rows, in iwd order, joining the signal // (from ordered) with name/type/known/connected (from managed). Carries the // D-Bus object `path` so the service can call Connect on the right object. // [{ id, ssid, security, signal:0..4, connected, known, needsPassphrase, path }] function networkRows(managed, ordered) { var nets = (managed && managed.networks) ? managed.networks : {}; var list = Array.isArray(ordered) ? ordered : []; var rows = []; var used = {}; function identity(net) { return String(net.ssid || "") + "\u0000" + String(net.security || ""); } function append(path, net, rawSignal, atFront) { if (!net || used[path]) return; var row = { id: networkId(net.ssid), ssid: net.ssid, security: net.security, signal: rawSignal === null ? 0 : signalBars(rawSignal), signalDbm: rawSignal === null ? null : dbmFromCenti(rawSignal), connected: net.connected, known: net.known, needsPassphrase: needsPassphrase(net.security, net.known), path: path }; used[path] = true; if (atFront) rows.unshift(row); else rows.push(row); } for (var i = 0; i < list.length; i++) { var orderedRow = list[i]; var path = orderedRow.path; var net = nets[path]; if (!net && (orderedRow.ssid || orderedRow.security)) { var wanted = identity(orderedRow); for (var candidate in nets) { if (!used[candidate] && identity(nets[candidate]) === wanted) { path = candidate; net = nets[candidate]; break; } } } append(path, net, orderedRow.signal, false); } // ObjectManager and Station snapshots can race. Retain unmatched managed // rows rather than flashing an empty model; a missing connected row leads. for (var managedPath in nets) if (!used[managedPath] && nets[managedPath].connected) append(managedPath, nets[managedPath], null, true); for (var fallbackPath in nets) if (!used[fallbackPath]) append(fallbackPath, nets[fallbackPath], null, false); return rows; } // --- Shaping / view helpers ------------------------------------------------ // needsPassphrase(security, known) — secured AND iwd has no stored secret. function needsPassphrase(security, known) { return security !== "open" && !known; } // signalRole(bars) — theme token; weak/medium/strong. function signalRole(bars) { if (bars <= 1) return "ui.minor"; if (bars <= 3) return "ui.accent"; return "content.accent"; } // --- Nerd Font md-wifi_strength glyphs (astral codepoints). Built from // codepoints to avoid hand-written surrogate pairs. --- function fromCodePoint(n) { if (n < 0x10000) return String.fromCharCode(n); var c = n - 0x10000; return String.fromCharCode(0xD800 + (c >> 10), 0xDC00 + (c & 0x3FF)); } var WIFI_GLYPHS = [ fromCodePoint(0xF092F), // 0: wifi-strength-outline fromCodePoint(0xF091F), // 1 fromCodePoint(0xF0922), // 2 fromCodePoint(0xF0925), // 3 fromCodePoint(0xF0928) // 4 ]; // signalGlyph(bars) — nerd-font wifi glyph by strength. function signalGlyph(bars) { var b = bars; if (b < 0) b = 0; if (b > 4) b = 4; return WIFI_GLYPHS[b]; } // securityLabel(sec) — human label for the security token. function securityLabel(sec) { if (sec === "open") return "open"; if (sec === "psk") return "wpa"; if (sec === "8021x") return "enterprise"; return sec ? sec : ""; } // networkId(ssid) — stable id (ssid itself; never empty). function networkId(ssid) { var s = (ssid === undefined || ssid === null) ? "" : String(ssid); return s.length > 0 ? s : "hidden-network"; } // --- Connection detail (StationDiagnostic.GetDiagnostics) ------------------ // Only the CONNECTED network has these; iwd exposes no per-BSS detail for // other scanned networks. The expand drawer shows bars/security for any row // and these extra fields only for the connected one. // bandLabel(freqMHz) — Wi-Fi band from centre frequency. "" if unknown. function bandLabel(freqMHz) { var f = Number(freqMHz); if (!isFinite(f) || f <= 0) return ""; if (f >= 5925) return "6 GHz"; if (f >= 4900) return "5 GHz"; if (f >= 2400) return "2.4 GHz"; return ""; } // rateLabel(raw) — iwd Rx/TxBitrate is in units of 100 kbit/s. -> "135 Mbps". // "" for missing/zero so the view can omit it. function rateLabel(raw) { var v = Number(raw); if (!isFinite(v) || v <= 0) return ""; return Math.round(v / 10) + " Mbps"; } // connectionDetail(rawDiag) -> { bssid, band, channel, rssi, rxRate, txRate, // mode } | null. rawDiag is the StationDiagnostic.GetDiagnostics a{sv} reply. // Returns null when there is no connected BSS (disconnected / no diagnostics). function connectionDetail(rawDiag) { var d = unwrap(rawDiag); if (!d || typeof d !== "object") return null; var bssid = prop(d, "ConnectedBss", ""); if (!bssid) return null; return { bssid: String(bssid), band: bandLabel(prop(d, "Frequency", 0)), channel: Number(prop(d, "Channel", 0)) || null, rssi: Number(prop(d, "RSSI", 0)) || null, rxRate: rateLabel(prop(d, "RxBitrate", 0)), txRate: rateLabel(prop(d, "TxBitrate", 0)), mode: String(prop(d, "RxMode", "") || "") }; } if (typeof module !== 'undefined' && module.exports) { module.exports = { unwrap: unwrap, prop: prop, signalBars: signalBars, dbmFromCenti: dbmFromCenti, parseManaged: parseManaged, parseOrdered: parseOrdered, networkRows: networkRows, needsPassphrase: needsPassphrase, signalRole: signalRole, signalGlyph: signalGlyph, securityLabel: securityLabel, networkId: networkId, bandLabel: bandLabel, rateLabel: rateLabel, connectionDetail: connectionDetail }; }