diff --git a/backend/src/config.js b/backend/src/config.js deleted file mode 100644 index 338d09b18..000000000 --- a/backend/src/config.js +++ /dev/null @@ -1,38 +0,0 @@ -import { publish, procedure } from "../../lib/mqtt.js" - -import { - connectToWifi, - DeviceWireless, - getWifis, - scan, -} from "../../lib/network.js" - -async function publishAccessPoints() { - try { - const wifis = await getWifis() - publish("config/wifis", wifis, null, { retain: true }) - } catch (err) { - console.error(err) - } -} - -await DeviceWireless.subscribe("AccessPointAdded", (/*access_point*/) => { - publishAccessPoints() -}) - -await DeviceWireless.subscribe("AccessPointRemoved", (/*access_point*/) => { - publishAccessPoints() -}) - -await procedure("config/wifis/scan", async () => { - await scan() -}) - -await procedure("config/wifis/connect", async (data) => { - await connectToWifi(data.path) -}) -;(async () => { - await publishAccessPoints() - await scan() - await publishAccessPoints() -})() diff --git a/backend/src/network/README.md b/backend/src/network/README.md new file mode 100644 index 000000000..a777491bb --- /dev/null +++ b/backend/src/network/README.md @@ -0,0 +1,56 @@ +# network service + +This is an MQTT service that is started as part of `bakcend`. + +### API + +### list + +**topic** `network/wifi/list` + +**payload:** +```json +[ + { + "ssid": "FOO", + "frequency": 2412, + "strength": 55, + "path": "/org/freedesktop/NetworkManager/AccessPoint/23" + }, + { + "ssid": "BAR", + "frequency": 5500, + "strength": 75, + "path": "/org/freedesktop/NetworkManager/AccessPoint/24" + } + ... +] +``` + +Subscribe to this topic to get the list of available access points. +The list will be automatically published to new subscribers and can be updated at any point. + +See `scan` below to request an explicit update. + +### scan + +**topic** `network/wifi/scan` + +Publish a request to this topic to have the wireless device scan for networks. +The response is emitted once the scan is done and contains no payload. + +This is likely to cause an update on `network/wifi/list`. + +### connect + +**topic** `network/wifi/connect` + +**payload:** +```json +{ + "path": "/org/freedesktop/NetworkManager/AccessPoint/24" // a path from the list of wifis +} +``` + +Publish a request to this topic to have the wireless device connect to the specified access point. +The response is emitted once the connection is established and contains no payload. diff --git a/backend/src/network/network.js b/backend/src/network/network.js new file mode 100644 index 000000000..43f82867e --- /dev/null +++ b/backend/src/network/network.js @@ -0,0 +1,44 @@ +import { publish, procedure } from "../../../lib/mqtt.js" + +import { NetworkManager } from "../../../lib/network.js" + +const networkmanager = new NetworkManager() +await networkmanager.init() + +async function publishAccessPoints() { + try { + const wifis = await networkmanager.getWifis() + publish("network/wifi/list", wifis, null, { retain: true }) + } catch (err) { + console.error(err) + } +} + +await networkmanager.DeviceWireless.subscribe( + "AccessPointAdded", + (/*access_point*/) => { + publishAccessPoints() + }, +) + +await networkmanager.DeviceWireless.subscribe( + "AccessPointRemoved", + (/*access_point*/) => { + publishAccessPoints() + }, +) + +await procedure("network/wifi/scan", async () => { + await networkmanager.scan() +}) + +await procedure("network/wifi/connect", async (data) => { + await networkmanager.connectToWifi(data.path) +}) + +// +;(async () => { + await publishAccessPoints() + await networkmanager.scan() + await publishAccessPoints() +})() diff --git a/backend/src/service.js b/backend/src/service.js index a7b5cf4a0..9c4285e1b 100755 --- a/backend/src/service.js +++ b/backend/src/service.js @@ -6,7 +6,7 @@ import express from "express" import cors from "cors" import "./factory.js" -import "./config.js" +import "./network/network.js" import "./led-operating-time.js" import { readSoftwareConfig, removeConfig } from "../../lib/file-config.js" import { capture } from "../../lib/scope.js" diff --git a/lib/helpers.js b/lib/helpers.js index acf0ee15a..f76569de5 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -1,3 +1,6 @@ +import { opendir } from "fs/promises" +import { join } from "path" + export function createNodeFromAsync(type, fn, input, output) { return function (RED) { function Node(config) { @@ -22,9 +25,6 @@ export function createNodeFromAsync(type, fn, input, output) { } } -import { opendir } from "fs/promises" -import { join } from "path" - export async function* walk(dir) { let fsdir try { diff --git a/lib/helpers.test.js b/lib/helpers.test.js index aaeff038f..fddfeac14 100644 --- a/lib/helpers.test.js +++ b/lib/helpers.test.js @@ -1,7 +1,8 @@ import { test } from "node:test" -import { cache } from "./helpers.js" import { setTimeout } from "node:timers/promises" +import { cache } from "./helpers.js" + test("cached sync", async (t) => { const fn = t.mock.fn(() => "foo") diff --git a/lib/network.js b/lib/network.js index 00f1eddeb..9c93908e8 100644 --- a/lib/network.js +++ b/lib/network.js @@ -2,51 +2,108 @@ import { systemBus } from "dbus.js" -const service = systemBus().getService("org.freedesktop.NetworkManager") +export class NetworkManager { + system_bus = null + network_manager = null + service = null -const NetworkManager = await service.getInterface( - "/org/freedesktop/NetworkManager", - "org.freedesktop.NetworkManager", -) + async init() { + if (this.network_manager) return -const [device_path] = await NetworkManager.GetDeviceByIpIface("wlan0") + const system_bus = systemBus() + const service = system_bus.getService("org.freedesktop.NetworkManager") -const [DeviceWireless, DeviceWireless_Properties] = await Promise.all([ - service.getInterface( - device_path, - "org.freedesktop.NetworkManager.Device.Wireless", - ), - service.getInterface(device_path, "org.freedesktop.DBus.Properties"), -]) + const network_manager = await service.getInterface( + "/org/freedesktop/NetworkManager", + "org.freedesktop.NetworkManager", + ) + + const [device_path] = await network_manager.GetDeviceByIpIface("wlan0") + const [DeviceWireless, DeviceWireless_Properties] = await Promise.all([ + service.getInterface( + device_path, + "org.freedesktop.NetworkManager.Device.Wireless", + ), + service.getInterface(device_path, "org.freedesktop.DBus.Properties"), + ]) + + Object.assign(this, { + system_bus, + service, + network_manager, + device_path, + DeviceWireless, + DeviceWireless_Properties, + }) + } + + // TODO: real async + async deinit() { + this.system_bus?.connection.end() + this.system_bus = null + this.network_manager = null + this.service.bus.connection.end() + this.service = null + } -export { DeviceWireless } + async scan() { + const { DeviceWireless_Properties, DeviceWireless } = this -export async function scan() { - const deferred = Promise.withResolvers() + const deferred = Promise.withResolvers() - // > To know when the scan is finished, use the "PropertiesChanged" signal from "org.freedesktop.DBus.Properties" to listen to changes to the "LastScan" property. - // https://networkmanager.dev/docs/api/latest/gdbus-org.freedesktop.NetworkManager.Device.Wireless.html#gdbus-method-org-freedesktop-NetworkManager-Device-Wireless.RequestScan - function handler(interface_name, changed_properties) { - if (interface_name !== "org.freedesktop.NetworkManager.Device.Wireless") { - return + // > To know when the scan is finished, use the "PropertiesChanged" signal from "org.freedesktop.DBus.Properties" to listen to changes to the "LastScan" property. + // https://networkmanager.dev/docs/api/latest/gdbus-org.freedesktop.NetworkManager.Device.Wireless.html#gdbus-method-org-freedesktop-NetworkManager-Device-Wireless.RequestScan + function handler(interface_name, changed_properties) { + if (interface_name !== "org.freedesktop.NetworkManager.Device.Wireless") { + return + } + + const LastScan = changed_properties.find((changed_property) => { + const [property_name] = changed_property + return property_name === "LastScan" + }) + if (!LastScan) return + + deferred.resolve() + DeviceWireless_Properties.unsubscribe("PropertiesChanged", handler).catch( + () => {}, + ) } + await DeviceWireless_Properties.subscribe("PropertiesChanged", handler) - const LastScan = changed_properties.find((changed_property) => { - const [property_name] = changed_property - return property_name === "LastScan" - }) - if (!LastScan) return + await DeviceWireless.RequestScan({}) + await deferred.promise + } - DeviceWireless_Properties.unsubscribe("PropertiesChanged", handler).then( - deferred.resolve, - deferred.reject, + async getWifis() { + const [access_point_paths] = await this.DeviceWireless.GetAllAccessPoints() + + const access_points = await Promise.all( + access_point_paths.map((access_point_path) => { + return this.service.getInterface( + access_point_path, + "org.freedesktop.NetworkManager.AccessPoint", + ) + }), ) - } - await DeviceWireless_Properties.subscribe("PropertiesChanged", handler) - await DeviceWireless.RequestScan({}) + return Promise.all( + access_points.map(async (access_point) => { + const [Ssid, frequency, strength] = await Promise.all( + ["Ssid", "Frequency", "Strength"].map((propName) => + readProp(access_point, propName), + ), + ) + const ssid = new TextDecoder().decode(Ssid) + const path = access_point.$parent.name + return { ssid, frequency, strength, path } + }), + ) + } - return deferred.promise + async connectToWifi(path) { + await NetworkManager.AddAndActivateConnection([], this.device_path, path) + } } async function readProp(iface, propName) { @@ -62,32 +119,11 @@ async function readProp(iface, propName) { return val[0][1][0] } -export async function getWifis() { - const [access_point_paths] = await DeviceWireless.GetAllAccessPoints() - - const access_points = await Promise.all( - access_point_paths.map((access_point_path) => { - return service.getInterface( - access_point_path, - "org.freedesktop.NetworkManager.AccessPoint", - ) - }), - ) - - return Promise.all( - access_points.map(async (access_point) => { - const [Ssid, frequency, strength] = await Promise.all( - ["Ssid", "Frequency", "Strength"].map((propName) => - readProp(access_point, propName), - ), - ) - const ssid = new TextDecoder().decode(Ssid) - const path = access_point.$parent.name - return { ssid, frequency, strength, path } - }), - ) -} - -export async function connectToWifi(path) { - await NetworkManager.AddAndActivateConnection([], device_path, path) +if (import.meta.main) { + const networkmanager = new NetworkManager() + await networkmanager.init() + await networkmanager.scan() + const wifis = await networkmanager.getWifis() + console.log(wifis) + await networkmanager.deinit() } diff --git a/lib/network.test.js b/lib/network.test.js new file mode 100644 index 000000000..f193cbd79 --- /dev/null +++ b/lib/network.test.js @@ -0,0 +1,32 @@ +import { describe, test, before, after } from "node:test" +import { readFile } from "node:fs/promises" + +import { NetworkManager } from "./network.js" + +let networkmanager = null + +before(async () => { + networkmanager = new NetworkManager() + await networkmanager.init() +}) + +test("scan", async () => { + await networkmanager.scan() +}) + +describe("getWifis", async () => { + test("returns PlanktoScope own wifi", async (t) => { + const machine_name = await readFile("/var/run/machine-name", "utf8") + + const wifis = await networkmanager.getWifis() + const wifi = wifis.find( + (wifi) => wifi.ssid == `PlanktoScope ${machine_name}`, + ) + t.assert.ok(wifi) + }) +}) + +after(async () => { + await networkmanager.deinit() + networkmanager = null +}) diff --git a/lib/nodered.js b/lib/nodered.js index 8f0bb156c..94956f773 100644 --- a/lib/nodered.js +++ b/lib/nodered.js @@ -1,4 +1,5 @@ -import waitPort from "wait-port" +import { promisePortOpen } from "promise-port" + import { createRequire } from "node:module" const require = createRequire(import.meta.url) @@ -7,7 +8,5 @@ const node_red_settings_path = "/home/pi/PlanktoScope/node-red/settings.cjs" export async function promiseDashboardOnline() { const { uiPort: port } = require(node_red_settings_path) - await waitPort({ - port, - }) + await promisePortOpen(port) } diff --git a/lib/package-lock.json b/lib/package-lock.json index f47fc511a..07dc077c0 100644 --- a/lib/package-lock.json +++ b/lib/package-lock.json @@ -1,11 +1,9 @@ { "name": "lib", - "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "lib", "dependencies": { "@js-temporal/polyfill": "^0.5.1", "csv-parse": "^6.1.0",