Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 0 additions & 38 deletions backend/src/config.js

This file was deleted.

56 changes: 56 additions & 0 deletions backend/src/network/README.md
Original file line number Diff line number Diff line change
@@ -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.
44 changes: 44 additions & 0 deletions backend/src/network/network.js
Original file line number Diff line number Diff line change
@@ -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()
})()
2 changes: 1 addition & 1 deletion backend/src/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 3 additions & 3 deletions lib/helpers.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion lib/helpers.test.js
Original file line number Diff line number Diff line change
@@ -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")

Expand Down
158 changes: 97 additions & 61 deletions lib/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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()
}
32 changes: 32 additions & 0 deletions lib/network.test.js
Original file line number Diff line number Diff line change
@@ -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
})
Loading
Loading