[rdkb][common][bsp][Add hostapd ucode support]

[Description]
Add hostapd ucode support for wifi7

[Release-log]

Change-Id: Ic4c3cca4a36a11fc3f9f9fdc94842b66a00721bf
diff --git a/recipes-wifi/hostapd/files/003-rdkb-uc-script-support.patch b/recipes-wifi/hostapd/files/003-rdkb-uc-script-support.patch
new file mode 100644
index 0000000..a6d1aba
--- /dev/null
+++ b/recipes-wifi/hostapd/files/003-rdkb-uc-script-support.patch
@@ -0,0 +1,228 @@
+From 85f654326e723173bc1c8329624ecbf75a0f98be Mon Sep 17 00:00:00 2001
+From: mtk27745 <rex.lu@mediatek.com>
+Date: Wed, 25 Oct 2023 16:20:01 +0800
+Subject: [PATCH] rdkb uc script support
+
+---
+ common.uc         | 22 ++++++++++++----------
+ hostapd.uc        |  6 +++---
+ wdev.uc           | 20 ++++++++++----------
+ wpa_supplicant.uc |  4 ++--
+ 4 files changed, 27 insertions(+), 25 deletions(-)
+
+diff --git a/common.uc b/common.uc
+index ccffe3e..2b48d3e 100644
+--- a/common.uc
++++ b/common.uc
+@@ -1,6 +1,6 @@
+ import * as nl80211 from "nl80211";
+ import * as rtnl from "rtnl";
+-import { readfile, glob, basename, readlink } from "fs";
++import * as fs from "fs";
+ 
+ const iftypes = {
+ 	ap: nl80211.const.NL80211_IFTYPE_AP,
+@@ -19,12 +19,14 @@ function __phy_is_fullmac(phyidx)
+ {
+ 	let data = nl80211.request(nl80211.const.NL80211_CMD_GET_WIPHY, 0, { wiphy: phyidx });
+ 
++	if (data == null)
++		return 1;
+ 	return !data.software_iftypes.ap_vlan;
+ }
+ 
+ function phy_is_fullmac(phy)
+ {
+-	let phyidx = int(trim(readfile(`/sys/class/ieee80211/${phy}/index`)));
++	let phyidx = int(trim(fs.readfile(`/sys/class/ieee80211/${phy}/index`)));
+ 
+ 	return __phy_is_fullmac(phyidx);
+ }
+@@ -39,14 +41,14 @@ function find_reusable_wdev(phyidx)
+ 		nl80211.const.NLM_F_DUMP,
+ 		{ wiphy: phyidx });
+ 	for (let res in data)
+-		if (trim(readfile(`/sys/class/net/${res.ifname}/operstate`)) == "down")
++		if (trim(fs.readfile(`/sys/class/net/${res.ifname}/operstate`)) == "down")
+ 			return res.ifname;
+ 	return null;
+ }
+ 
+ function wdev_create(phy, name, data)
+ {
+-	let phyidx = int(readfile(`/sys/class/ieee80211/${phy}/index`));
++	let phyidx = int(fs.readfile(`/sys/class/ieee80211/${phy}/index`));
+ 
+ 	wdev_remove(name);
+ 
+@@ -96,7 +98,7 @@ function wdev_create(phy, name, data)
+ 
+ function phy_sysfs_file(phy, name)
+ {
+-	return trim(readfile(`/sys/class/ieee80211/${phy}/${name}`));
++	return trim(fs.readfile(`/sys/class/ieee80211/${phy}/${name}`));
+ }
+ 
+ function macaddr_split(str)
+@@ -111,7 +113,7 @@ function macaddr_join(addr)
+ 
+ function wdev_macaddr(wdev)
+ {
+-	return trim(readfile(`/sys/class/net/${wdev}/address`));
++	return trim(fs.readfile(`/sys/class/net/${wdev}/address`));
+ }
+ 
+ const phy_proto = {
+@@ -221,10 +223,10 @@ const phy_proto = {
+ 	},
+ 
+ 	for_each_wdev: function(cb) {
+-		let wdevs = glob(`/sys/class/ieee80211/${this.name}/device/net/*`);
+-		wdevs = map(wdevs, (arg) => basename(arg));
++		let wdevs = fs.glob(`/sys/class/ieee80211/${this.name}/device/net/*`);
++		wdevs = map(wdevs, (arg) => fs.basename(arg));
+ 		for (let wdev in wdevs) {
+-			if (basename(readlink(`/sys/class/net/${wdev}/phy80211`)) != this.name)
++			if (fs.basename(fs.readlink(`/sys/class/net/${wdev}/phy80211`)) != this.name)
+ 				continue;
+ 
+ 			cb(wdev);
+@@ -234,7 +236,7 @@ const phy_proto = {
+ 
+ function phy_open(phy)
+ {
+-	let phyidx = readfile(`/sys/class/ieee80211/${phy}/index`);
++	let phyidx = fs.readfile(`/sys/class/ieee80211/${phy}/index`);
+ 	if (!phyidx)
+ 		return null;
+ 
+diff --git a/hostapd.uc b/hostapd.uc
+index 1bce754..5f5ca35 100644
+--- a/hostapd.uc
++++ b/hostapd.uc
+@@ -1,5 +1,5 @@
+ let libubus = require("ubus");
+-import { open, readfile } from "fs";
++import * as fs from "fs";
+ import { wdev_create, wdev_remove, is_equal, vlist_new, phy_is_fullmac, phy_open } from "common";
+ 
+ let ubus = libubus.connect();
+@@ -562,7 +562,7 @@ function config_add_bss(config, name)
+ 
+ function iface_load_config(filename)
+ {
+-	let f = open(filename, "r");
++	let f = fs.open(filename, "r");
+ 	if (!f)
+ 		return null;
+ 
+@@ -620,7 +620,7 @@ function iface_load_config(filename)
+ 		}
+ 
+ 		if (hostapd.data.file_fields[val[0]])
+-			bss.hash[val[0]] = hostapd.sha1(readfile(val[1]));
++			bss.hash[val[0]] = hostapd.sha1(fs.readfile(val[1]));
+ 
+ 		push(bss.data, line);
+ 	}
+diff --git a/wdev.uc b/wdev.uc
+index cf438f7..5eb5e3c 100644
+--- a/wdev.uc
++++ b/wdev.uc
+@@ -1,7 +1,7 @@
+ #!/usr/bin/env ucode
+ 'use strict';
+ import { vlist_new, is_equal, wdev_create, wdev_remove, phy_open } from "/usr/share/hostap/common.uc";
+-import { readfile, writefile, basename, readlink, glob } from "fs";
++import * as fs from "fs";
+ let libubus = require("ubus");
+ 
+ let keep_devices = {};
+@@ -32,7 +32,7 @@ function iface_start(wdev)
+ {
+ 	let ifname = wdev.ifname;
+ 
+-	if (readfile(`/sys/class/net/${ifname}/ifindex`)) {
++	if (fs.readfile(`/sys/class/net/${ifname}/ifindex`)) {
+ 		system([ "ip", "link", "set", "dev", ifname, "down" ]);
+ 		wdev_remove(ifname);
+ 	}
+@@ -89,7 +89,7 @@ function iface_cb(new_if, old_if)
+ function drop_inactive(config)
+ {
+ 	for (let key in config) {
+-		if (!readfile(`/sys/class/net/${key}/ifindex`))
++		if (!fs.readfile(`/sys/class/net/${key}/ifindex`))
+ 			delete config[key];
+ 	}
+ }
+@@ -108,23 +108,23 @@ function delete_ifname(config)
+ 
+ function add_existing(phy, config)
+ {
+-	let wdevs = glob(`/sys/class/ieee80211/${phy}/device/net/*`);
+-	wdevs = map(wdevs, (arg) => basename(arg));
++	let wdevs = fs.glob(`/sys/class/ieee80211/${phy}/device/net/*`);
++	wdevs = map(wdevs, (arg) => fs.basename(arg));
+ 	for (let wdev in wdevs) {
+ 		if (config[wdev])
+ 			continue;
+ 
+-		if (basename(readlink(`/sys/class/net/${wdev}/phy80211`)) != phy)
++		if (fs.basename(fs.readlink(`/sys/class/net/${wdev}/phy80211`)) != phy)
+ 			continue;
+ 
+-		if (trim(readfile(`/sys/class/net/${wdev}/operstate`)) == "down")
++		if (trim(fs.readfile(`/sys/class/net/${wdev}/operstate`)) == "down")
+ 			config[wdev] = {};
+ 	}
+ }
+ 
+ function usage()
+ {
+-	warn(`Usage: ${basename(sourcepath())} <phy> <command> [<arguments>]
++	warn(`Usage: ${fs.basename(sourcepath())} <phy> <command> [<arguments>]
+ 
+ Commands:
+ 	set_config <config> [<device]...] - set phy configuration
+@@ -150,7 +150,7 @@ const commands = {
+ 			exit(1);
+ 		}
+ 
+-		let old_config = readfile(statefile);
++		let old_config = fs.readfile(statefile);
+ 		if (old_config)
+ 			old_config = json(old_config);
+ 
+@@ -175,7 +175,7 @@ const commands = {
+ 
+ 		drop_inactive(config.data);
+ 		delete_ifname(config.data);
+-		writefile(statefile, sprintf("%J", config.data));
++		fs.writefile(statefile, sprintf("%J", config.data));
+ 	},
+ 	get_macaddr: function(args) {
+ 		let data = {};
+diff --git a/wpa_supplicant.uc b/wpa_supplicant.uc
+index 2a9de67..cd149dc 100644
+--- a/wpa_supplicant.uc
++++ b/wpa_supplicant.uc
+@@ -1,5 +1,5 @@
+ let libubus = require("ubus");
+-import { open, readfile } from "fs";
++import * as fs from "fs";
+ import { wdev_create, wdev_remove, is_equal, vlist_new, phy_open } from "common";
+ 
+ let ubus = libubus.connect();
+@@ -62,7 +62,7 @@ function iface_cb(new_if, old_if)
+ 
+ function prepare_config(config)
+ {
+-	config.config_data = readfile(config.config);
++	config.config_data = fs.readfile(config.config);
+ 
+ 	return { config: config };
+ }
+-- 
+2.18.0
+
diff --git a/recipes-wifi/hostapd/files/common.uc b/recipes-wifi/hostapd/files/common.uc
index 9ece3b1..ccffe3e 100644
--- a/recipes-wifi/hostapd/files/common.uc
+++ b/recipes-wifi/hostapd/files/common.uc
@@ -1,6 +1,6 @@
 import * as nl80211 from "nl80211";
 import * as rtnl from "rtnl";
-import { readfile } from "fs";
+import { readfile, glob, basename, readlink } from "fs";
 
 const iftypes = {
 	ap: nl80211.const.NL80211_IFTYPE_AP,
@@ -94,6 +94,156 @@
 	return null;
 }
 
+function phy_sysfs_file(phy, name)
+{
+	return trim(readfile(`/sys/class/ieee80211/${phy}/${name}`));
+}
+
+function macaddr_split(str)
+{
+	return map(split(str, ":"), (val) => hex(val));
+}
+
+function macaddr_join(addr)
+{
+	return join(":", map(addr, (val) => sprintf("%02x", val)));
+}
+
+function wdev_macaddr(wdev)
+{
+	return trim(readfile(`/sys/class/net/${wdev}/address`));
+}
+
+const phy_proto = {
+	macaddr_init: function(used, options) {
+		this.macaddr_options = options ?? {};
+		this.macaddr_list = {};
+
+		if (type(used) == "object")
+			for (let addr in used)
+				this.macaddr_list[addr] = used[addr];
+		else
+			for (let addr in used)
+				this.macaddr_list[addr] = -1;
+
+		this.for_each_wdev((wdev) => {
+			let macaddr = wdev_macaddr(wdev);
+			this.macaddr_list[macaddr] ??= -1;
+		});
+
+		return this.macaddr_list;
+	},
+
+	macaddr_generate: function(data) {
+		let phy = this.name;
+		let idx = int(data.id ?? 0);
+		let mbssid = int(data.mbssid ?? 0) > 0;
+		let num_global = int(data.num_global ?? 1);
+		let use_global = !mbssid && idx < num_global;
+
+		let base_addr = phy_sysfs_file(phy, "macaddress");
+		if (!base_addr)
+			return null;
+
+		if (!idx && !mbssid)
+			return base_addr;
+
+		let base_mask = phy_sysfs_file(phy, "address_mask");
+		if (!base_mask)
+			return null;
+
+		if (base_mask == "00:00:00:00:00:00" && idx >= num_global) {
+			let addrs = split(phy_sysfs_file(phy, "addresses"), "\n");
+
+			if (idx < length(addrs))
+				return addrs[idx];
+
+			base_mask = "ff:ff:ff:ff:ff:ff";
+		}
+
+		let addr = macaddr_split(base_addr);
+		let mask = macaddr_split(base_mask);
+		let type;
+
+		if (mbssid)
+			type = "b5";
+		else if (use_global)
+			type = "add";
+		else if (mask[0] > 0)
+			type = "b1";
+		else if (mask[5] < 0xff)
+			type = "b5";
+		else
+			type = "add";
+
+		switch (type) {
+		case "b1":
+			if (!(addr[0] & 2))
+				idx--;
+			addr[0] |= 2;
+			addr[0] ^= idx << 2;
+			break;
+		case "b5":
+			if (mbssid)
+				addr[0] |= 2;
+			addr[5] ^= idx;
+			break;
+		default:
+			for (let i = 5; i > 0; i--) {
+				addr[i] += idx;
+				if (addr[i] < 256)
+					break;
+				addr[i] %= 256;
+			}
+			break;
+		}
+
+		return macaddr_join(addr);
+	},
+
+	macaddr_next: function(val) {
+		let data = this.macaddr_options ?? {};
+		let list = this.macaddr_list;
+
+		for (let i = 0; i < 32; i++) {
+			data.id = i;
+
+			let mac = this.macaddr_generate(data);
+			if (!mac)
+				return null;
+
+			if (list[mac] != null)
+				continue;
+
+			list[mac] = val != null ? val : -1;
+			return mac;
+		}
+	},
+
+	for_each_wdev: function(cb) {
+		let wdevs = glob(`/sys/class/ieee80211/${this.name}/device/net/*`);
+		wdevs = map(wdevs, (arg) => basename(arg));
+		for (let wdev in wdevs) {
+			if (basename(readlink(`/sys/class/net/${wdev}/phy80211`)) != this.name)
+				continue;
+
+			cb(wdev);
+		}
+	}
+};
+
+function phy_open(phy)
+{
+	let phyidx = readfile(`/sys/class/ieee80211/${phy}/index`);
+	if (!phyidx)
+		return null;
+
+	return proto({
+		name: phy,
+		idx: int(phyidx)
+	}, phy_proto);
+}
+
 const vlist_proto = {
 	update: function(values, arg) {
 		let data = this.data;
@@ -150,7 +300,7 @@
 			if (!is_equal(val1[key], val2[key]))
 				return false;
 		for (let key in val2)
-			if (!val1[key])
+			if (val1[key] == null)
 				return false;
 		return true;
 	} else {
@@ -165,4 +315,4 @@
 		}, vlist_proto);
 }
 
-export { wdev_remove, wdev_create, is_equal, vlist_new, phy_is_fullmac };
+export { wdev_remove, wdev_create, is_equal, vlist_new, phy_is_fullmac, phy_open };
diff --git a/recipes-wifi/hostapd/files/hostapd.uc b/recipes-wifi/hostapd/files/hostapd.uc
index b52732a..1bce754 100644
--- a/recipes-wifi/hostapd/files/hostapd.uc
+++ b/recipes-wifi/hostapd/files/hostapd.uc
@@ -1,6 +1,6 @@
 let libubus = require("ubus");
 import { open, readfile } from "fs";
-import { wdev_create, wdev_remove, is_equal, vlist_new, phy_is_fullmac } from "common";
+import { wdev_create, wdev_remove, is_equal, vlist_new, phy_is_fullmac, phy_open } from "common";
 
 let ubus = libubus.connect();
 
@@ -26,12 +26,11 @@
 	if (!cfg || !cfg.bss || !cfg.bss[0] || !cfg.bss[0].ifname)
 		return;
 
-	hostapd.remove_iface(cfg.bss[0].ifname);
 	for (let bss in cfg.bss)
 		wdev_remove(bss.ifname);
 }
 
-function iface_gen_config(phy, config)
+function iface_gen_config(phy, config, start_disabled)
 {
 	let str = `data:
 ${join("\n", config.radio.data)}
@@ -41,18 +40,106 @@
 	for (let i = 0; i < length(config.bss); i++) {
 		let bss = config.bss[i];
 		let type = i > 0 ? "bss" : "interface";
+		let nasid = bss.nasid ?? replace(bss.bssid, ":", "");
 
 		str += `
 ${type}=${bss.ifname}
+bssid=${bss.bssid}
 ${join("\n", bss.data)}
+nas_identifier=${nasid}
+`;
+		if (start_disabled)
+			str += `
+start_disabled=1
 `;
 	}
 
 	return str;
 }
 
+function iface_freq_info(iface, config, params)
+{
+	let freq = params.frequency;
+	let bw320_offset = params.bw320_offset;
+	if (!freq)
+		return null;
+
+	let sec_offset = params.sec_chan_offset;
+	if (sec_offset != -1 && sec_offset != 1)
+		sec_offset = 0;
+
+	let width = 0;
+	if (params.ch_width >= 0){
+		width = params.ch_width;
+	} else {
+		for (let line in config.radio.data) {
+			if (!sec_offset && match(line, /^ht_capab=.*HT40/)) {
+				sec_offset = null; // auto-detect
+				continue;
+			}
+
+			let val = match(line, /^(vht_oper_chwidth|he_oper_chwidth|eht_oper_chwidth)=(\d+)/);
+			if (!val)
+				continue;
+
+			val = int(val[2]);
+			if (val > width)
+				width = val;
+		}
+	}
+
+	if (freq < 4000)
+		width = 0;
+
+	return hostapd.freq_info(freq, sec_offset, width, bw320_offset);
+}
+
+function iface_add(phy, config, phy_status)
+{
+	let config_inline = iface_gen_config(phy, config, !!phy_status);
+
+	let bss = config.bss[0];
+	let ret = hostapd.add_iface(`bss_config=${phy}:${config_inline}`);
+	if (ret < 0)
+		return false;
+
+	if (!phy_status)
+		return true;
+
+	let iface = hostapd.interfaces[phy];
+	if (!iface)
+		return false;
+
+	let freq_info = iface_freq_info(iface, config, phy_status);
+
+	return iface.start(freq_info) >= 0;
+}
+
-function iface_restart(phy, config, old_config)
+function iface_config_macaddr_list(config)
 {
+	let macaddr_list = {};
+	for (let i = 0; i < length(config.bss); i++) {
+		let bss = config.bss[i];
+		if (!bss.default_macaddr)
+			macaddr_list[bss.bssid] = i;
+	}
+
+	return macaddr_list;
+}
+
+function iface_update_supplicant_macaddr(phy, config)
+{
+	let macaddr_list = [];
+	for (let i = 0; i < length(config.bss); i++)
+		push(macaddr_list, config.bss[i].bssid);
+	ubus.call("wpa_supplicant", "phy_set_macaddr_list", { phy: phy, macaddr: macaddr_list });
+}
+
+function iface_restart(phydev, config, old_config)
+{
+	let phy = phydev.name;
+
+	hostapd.remove_iface(phy);
 	iface_remove(old_config);
 	iface_remove(config);
 
@@ -61,15 +148,33 @@
 		return;
 	}
 
+	phydev.macaddr_init(iface_config_macaddr_list(config));
+	for (let i = 0; i < length(config.bss); i++) {
+		let bss = config.bss[i];
+		if (bss.default_macaddr)
+			bss.bssid = phydev.macaddr_next();
+	}
+
+	iface_update_supplicant_macaddr(phy, config);
+
 	let bss = config.bss[0];
 	let err = wdev_create(phy, bss.ifname, { mode: "ap" });
 	if (err)
 		hostapd.printf(`Failed to create ${bss.ifname} on phy ${phy}: ${err}`);
-	let config_inline = iface_gen_config(phy, config);
-	if (hostapd.add_iface(`bss_config=${bss.ifname}:${config_inline}`) < 0) {
-		hostapd.printf(`hostapd.add_iface failed for phy ${phy} ifname=${bss.ifname}`);
-		return;
+
+	let ubus = hostapd.data.ubus;
+	let phy_status = ubus.call("wpa_supplicant", "phy_status", { phy: phy });
+	if (phy_status && phy_status.state == "COMPLETED") {
+		if (iface_add(phy, config, phy_status))
+			return;
+
+		hostapd.printf(`Failed to bring up phy ${phy} ifname=${bss.ifname} with supplicant provided frequency`);
 	}
+
+	ubus.call("wpa_supplicant", "phy_set_state", { phy: phy, stop: true });
+	if (!iface_add(phy, config))
+		hostapd.printf(`hostapd.add_iface failed for phy ${phy} ifname=${bss.ifname}`);
+	ubus.call("wpa_supplicant", "phy_set_state", { phy: phy, stop: false });
 }
 
 function array_to_obj(arr, key, start)
@@ -109,8 +214,65 @@
 	hostapd.printf(`Reload WPA PSK file for bss ${config.ifname}: ${ret}`);
 }
 
-function iface_reload_config(phy, config, old_config)
+function remove_file_fields(config)
 {
+	return filter(config, (line) => !hostapd.data.file_fields[split(line, "=")[0]]);
+}
+
+function bss_remove_file_fields(config)
+{
+	let new_cfg = {};
+
+	for (let key in config)
+		new_cfg[key] = config[key];
+	new_cfg.data = remove_file_fields(new_cfg.data);
+	new_cfg.hash = {};
+	for (let key in config.hash)
+		new_cfg.hash[key] = config.hash[key];
+	delete new_cfg.hash.wpa_psk_file;
+	delete new_cfg.hash.vlan_file;
+
+	return new_cfg;
+}
+
+function bss_config_hash(config)
+{
+	return hostapd.sha1(remove_file_fields(config) + "");
+}
+
+function bss_find_existing(config, prev_config, prev_hash)
+{
+	let hash = bss_config_hash(config.data);
+
+	for (let i = 0; i < length(prev_config.bss); i++) {
+		if (!prev_hash[i] || hash != prev_hash[i])
+			continue;
+
+		prev_hash[i] = null;
+		return i;
+	}
+
+	return -1;
+}
+
+function get_config_bss(config, idx)
+{
+	if (!config.bss[idx]) {
+		hostapd.printf(`Invalid bss index ${idx}`);
+		return null;
+	}
+
+	let ifname = config.bss[idx].ifname;
+	if (!ifname)
+		hostapd.printf(`Could not find bss ${config.bss[idx].ifname}`);
+
+	return hostapd.bss[ifname];
+}
+
+function iface_reload_config(phydev, config, old_config)
+{
+	let phy = phydev.name;
+
 	if (!old_config || !is_equal(old_config.radio, config.radio))
 		return false;
 
@@ -120,75 +282,230 @@
 	if (!old_config.bss || !old_config.bss[0])
 		return false;
 
-	if (config.bss[0].ifname != old_config.bss[0].ifname)
+	let iface = hostapd.interfaces[phy];
+	if (!iface) {
+		hostapd.printf(`Could not find previous interface ${iface_name}`);
 		return false;
+	}
 
-	let iface = hostapd.interfaces[config.bss[0].ifname];
-	if (!iface)
+	let iface_name = old_config.bss[0].ifname;
+	let first_bss = hostapd.bss[iface_name];
+	if (!first_bss) {
+		hostapd.printf(`Could not find bss of previous interface ${iface_name}`);
 		return false;
+	}
 
-	let config_inline = iface_gen_config(phy, config);
+	let macaddr_list = iface_config_macaddr_list(config);
+	let bss_list = [];
+	let bss_list_cfg = [];
+	let prev_bss_hash = [];
 
-	bss_reload_psk(iface.bss[0], config.bss[0], old_config.bss[0]);
-	if (!is_equal(config.bss[0], old_config.bss[0])) {
-		if (phy_is_fullmac(phy))
-			return false;
+	for (let bss in old_config.bss) {
+		let hash = bss_config_hash(bss.data);
+		push(prev_bss_hash, bss_config_hash(bss.data));
+	}
 
-		hostapd.printf(`Reload config for bss '${config.bss[0].ifname}' on phy '${phy}'`);
-		if (iface.bss[0].set_config(config_inline, 0) < 0) {
-			hostapd.printf(`Failed to set config`);
+	// Step 1: find (possibly renamed) interfaces with the same config
+	// and store them in the new order (with gaps)
+	for (let i = 0; i < length(config.bss); i++) {
+		let prev;
+
+		// For fullmac devices, the first interface needs to be preserved,
+		// since it's treated as the master
+		if (!i && phy_is_fullmac(phy)) {
+			prev = 0;
+			prev_bss_hash[0] = null;
+		} else {
+			prev = bss_find_existing(config.bss[i], old_config, prev_bss_hash);
+		}
+		if (prev < 0)
+			continue;
+
+		let cur_config = config.bss[i];
+		let prev_config = old_config.bss[prev];
+
+		let prev_bss = get_config_bss(old_config, prev);
+		if (!prev_bss)
 			return false;
+
+		// try to preserve MAC address of this BSS by reassigning another
+		// BSS if necessary
+		if (cur_config.default_macaddr &&
+		    !macaddr_list[prev_config.bssid]) {
+			macaddr_list[prev_config.bssid] = i;
+			cur_config.bssid = prev_config.bssid;
 		}
+
+		bss_list[i] = prev_bss;
+		bss_list_cfg[i] = old_config.bss[prev];
 	}
 
-	let bss_list = array_to_obj(iface.bss, "name", 1);
-	let new_cfg = array_to_obj(config.bss, "ifname", 1);
-	let old_cfg = array_to_obj(old_config.bss, "ifname", 1);
+	if (config.mbssid && !bss_list_cfg[0]) {
+		hostapd.printf("First BSS changed with MBSSID enabled");
+		return false;
+	}
 
-	for (let name in old_cfg) {
-		let bss = bss_list[name];
-		if (!bss) {
-			hostapd.printf(`bss '${name}' not found`);
-			return false;
+	// Step 2: if none were found, rename and preserve the first one
+	if (length(bss_list) == 0) {
+		// can't change the bssid of the first bss
+		if (config.bss[0].bssid != old_config.bss[0].bssid) {
+			if (!config.bss[0].default_macaddr) {
+				hostapd.printf(`BSSID of first interface changed: ${lc(old_config.bss[0].bssid)} -> ${lc(config.bss[0].bssid)}`);
+				return false;
+			}
+
+			config.bss[0].bssid = old_config.bss[0].bssid;
 		}
 
-		if (!new_cfg[name]) {
-			hostapd.printf(`Remove bss '${name}' on phy '${phy}'`);
-			bss.delete();
-			wdev_remove(name);
+		let prev_bss = get_config_bss(old_config, 0);
+		if (!prev_bss)
+			return false;
+
+		macaddr_list[config.bss[0].bssid] = 0;
+		bss_list[0] = prev_bss;
+		bss_list_cfg[0] = old_config.bss[0];
+		prev_bss_hash[0] = null;
+	}
+
+	// Step 3: delete all unused old interfaces
+	for (let i = 0; i < length(prev_bss_hash); i++) {
+		if (!prev_bss_hash[i])
 			continue;
-		}
 
-		let new_cfg_data = new_cfg[name];
-		delete new_cfg[name];
+		let prev_bss = get_config_bss(old_config, i);
+		if (!prev_bss)
+			return false;
+
+		let ifname = old_config.bss[i].ifname;
+		hostapd.printf(`Remove bss '${ifname}' on phy '${phy}'`);
+		prev_bss.delete();
+		wdev_remove(ifname);
+	}
 
-		if (is_equal(old_cfg[name], new_cfg_data))
+	// Step 4: rename preserved interfaces, use temporary name on duplicates
+	let rename_list = [];
+	for (let i = 0; i < length(bss_list); i++) {
+		if (!bss_list[i])
 			continue;
 
-		hostapd.printf(`Reload config for bss '${name}' on phy '${phy}'`);
-		let idx = find_array_idx(config.bss, "ifname", name);
-		if (idx < 0) {
-			hostapd.printf(`bss index not found`);
+		let old_ifname = bss_list_cfg[i].ifname;
+		let new_ifname = config.bss[i].ifname;
+		if (old_ifname == new_ifname)
+			continue;
+
+		if (hostapd.bss[new_ifname]) {
+			new_ifname = "tmp_" + substr(hostapd.sha1(new_ifname), 0, 8);
+			push(rename_list, i);
+		}
+
+		hostapd.printf(`Rename bss ${old_ifname} to ${new_ifname}`);
+		if (!bss_list[i].rename(new_ifname)) {
+			hostapd.printf(`Failed to rename bss ${old_ifname} to ${new_ifname}`);
 			return false;
 		}
 
+		bss_list_cfg[i].ifname = new_ifname;
+	}
+
-		if (bss.set_config(config_inline, idx) < 0) {
-			hostapd.printf(`Failed to set config`);
+	// Step 5: rename interfaces with temporary names
+	for (let i in rename_list) {
+		let new_ifname = config.bss[i].ifname;
+		if (!bss_list[i].rename(new_ifname)) {
+			hostapd.printf(`Failed to rename bss to ${new_ifname}`);
 			return false;
 		}
+		bss_list_cfg[i].ifname = new_ifname;
 	}
 
-	for (let name in new_cfg) {
-		hostapd.printf(`Add bss '${name}' on phy '${phy}'`);
+	// Step 6: assign BSSID for newly created interfaces
+	let macaddr_data = {
+		num_global: config.num_global_macaddr ?? 1,
+		mbssid: config.mbssid ?? 0,
+	};
+	macaddr_list = phydev.macaddr_init(macaddr_list, macaddr_data);
+	for (let i = 0; i < length(config.bss); i++) {
+		if (bss_list[i])
+			continue;
+		let bsscfg = config.bss[i];
+
+		let mac_idx = macaddr_list[bsscfg.bssid];
+		if (mac_idx < 0)
+			macaddr_list[bsscfg.bssid] = i;
+		if (mac_idx == i)
+			continue;
+
+		// statically assigned bssid of the new interface is in conflict
+		// with the bssid of a reused interface. reassign the reused interface
+		if (!bsscfg.default_macaddr) {
+			// can't update bssid of the first BSS, need to restart
+			if (!mac_idx < 0)
+				return false;
 
-		let idx = find_array_idx(config.bss, "ifname", name);
-		if (idx < 0) {
-			hostapd.printf(`bss index not found`);
+			bsscfg = config.bss[mac_idx];
+		}
+
+		let addr = phydev.macaddr_next(i);
+		if (!addr) {
+			hostapd.printf(`Failed to generate mac address for phy ${phy}`);
 			return false;
 		}
+		bsscfg.bssid = addr;
+	}
 
-		if (iface.add_bss(config_inline, idx) < 0) {
-			hostapd.printf(`Failed to add bss`);
+	let config_inline = iface_gen_config(phy, config);
+
+	// Step 7: fill in the gaps with new interfaces
+	for (let i = 0; i < length(config.bss); i++) {
+		let ifname = config.bss[i].ifname;
+		let bss = bss_list[i];
+
+		if (bss)
+			continue;
+
+		hostapd.printf(`Add bss ${ifname} on phy ${phy}`);
+		bss_list[i] = iface.add_bss(config_inline, i);
+		if (!bss_list[i]) {
+			hostapd.printf(`Failed to add new bss ${ifname} on phy ${phy}`);
+			return false;
+		}
+	}
+
+	// Step 8: update interface bss order
+	if (!iface.set_bss_order(bss_list)) {
+		hostapd.printf(`Failed to update BSS order on phy '${phy}'`);
+		return false;
+	}
+
+	// Step 9: update config
+	for (let i = 0; i < length(config.bss); i++) {
+		if (!bss_list_cfg[i])
+			continue;
+
+		let ifname = config.bss[i].ifname;
+		let bss = bss_list[i];
+
+		if (is_equal(config.bss[i], bss_list_cfg[i]))
+			continue;
+
+		if (is_equal(bss_remove_file_fields(config.bss[i]),
+		             bss_remove_file_fields(bss_list_cfg[i]))) {
+			hostapd.printf(`Update config data files for bss ${ifname}`);
+			if (bss.set_config(config_inline, i, true) < 0) {
+				hostapd.printf(`Could not update config data files for bss ${ifname}`);
+				return false;
+			} else {
+				bss.ctrl("RELOAD_WPA_PSK");
+				continue;
+			}
+		}
+
+		bss_reload_psk(bss, config.bss[i], bss_list_cfg[i]);
+		if (is_equal(config.bss[i], bss_list_cfg[i]))
+			continue;
+
+		hostapd.printf(`Reload config for bss '${config.bss[0].ifname}' on phy '${phy}'`);
+		if (bss.set_config(config_inline, i) < 0) {
+			hostapd.printf(`Failed to set config for bss ${ifname}`);
 			return false;
 		}
 	}
@@ -202,17 +519,32 @@
 
 	hostapd.data.config[phy] = config;
 
-	if (!config)
+	if (!config) {
+		hostapd.remove_iface(phy);
 		return iface_remove(old_config);
+	}
+
+	let phydev = phy_open(phy);
+	if (!phydev) {
+		hostapd.printf(`Failed to open phy ${phy}`);
+		return false;
+	}
 
-	let ret = iface_reload_config(phy, config, old_config);
-	if (ret) {
-		hostapd.printf(`Reloaded settings for phy ${phy}`);
-		return 0;
+	try {
+		let ret = iface_reload_config(phydev, config, old_config);
+		if (ret) {
+			iface_update_supplicant_macaddr(phy, config);
+			hostapd.printf(`Reloaded settings for phy ${phy}`);
+			return 0;
+		}
+	} catch (e) {
+			hostapd.printf(`Error reloading config: ${e}\n${e.stacktrace[0].context}`);
 	}
 
 	hostapd.printf(`Restart interface for phy ${phy}`);
-	return iface_restart(phy, config, old_config);
+	let ret = iface_restart(phydev, config, old_config);
+
+	return ret;
 }
 
 function config_add_bss(config, name)
@@ -259,14 +591,29 @@
 			continue;
 		}
 
+		if (val[0] == "#num_global_macaddr" ||
+		    val[0] == "mbssid")
+			config[val[0]] = int(val[1]);
+
 		push(config.radio.data, line);
 	}
 
 	while ((line = trim(f.read("line"))) != null) {
+		if (line == "#default_macaddr")
+			bss.default_macaddr = true;
+
 		let val = split(line, "=", 2);
 		if (!val[0])
 			continue;
 
+		if (val[0] == "bssid") {
+			bss.bssid = lc(val[1]);
+			continue;
+		}
+
+		if (val[0] == "nas_identifier")
+			bss.nasid = val[1];
+
 		if (val[0] == "bss") {
 			bss = config_add_bss(config, val[1]);
 			continue;
@@ -282,28 +629,112 @@
 	return config;
 }
 
-
+function ex_wrap(func) {
+	return (req) => {
+		try {
+			let ret = func(req);
+			return ret;
+		} catch(e) {
+			hostapd.printf(`Exception in ubus function: ${e}\n${e.stacktrace[0].context}`);
+		}
+		return libubus.STATUS_UNKNOWN_ERROR;
+	};
+}
 
 let main_obj = {
 	reload: {
 		args: {
 			phy: "",
+		},
+		call: ex_wrap(function(req) {
+			let phy_list = req.args.phy ? [ req.args.phy ] : keys(hostapd.data.config);
+			for (let phy_name in phy_list) {
+				let phy = hostapd.data.config[phy_name];
+				let config = iface_load_config(phy.orig_file);
+				iface_set_config(phy_name, config);
+			}
+
+			return 0;
+		})
+	},
+	apsta_state: {
+		args: {
+			phy: "",
+			up: true,
+			frequency: 0,
+			sec_chan_offset: 0,
+			ch_width: -1,
+			bw320_offset: 1,
+			csa: true,
+			csa_count: 0,
 		},
-		call: function(req) {
-			try {
-				let phy_list = req.args.phy ? [ req.args.phy ] : keys(hostapd.data.config);
-				for (let phy_name in phy_list) {
-					let phy = hostapd.data.config[phy_name];
-					let config = iface_load_config(phy.orig_file);
-					iface_set_config(phy_name, config);
-				}
-			} catch(e) {
-				hostapd.printf(`Error reloading config: ${e}\n${e.stacktrace[0].context}`);
+		call: ex_wrap(function(req) {
+			if (req.args.up == null || !req.args.phy)
 				return libubus.STATUS_INVALID_ARGUMENT;
+
+			hostapd.printf(`ucode: mtk: apsta state update`);
+			hostapd.printf(`    * phy: ${req.args.phy}`);
+			hostapd.printf(`    * up: ${req.args.up}`);
+			hostapd.printf(`    * freqeuncy: ${req.args.frequency}`);
+			hostapd.printf(`    * sec_chan_offset: ${req.args.sec_chan_offset}`);
+			hostapd.printf(`    * ch_width: ${req.args.ch_width}`);
+			hostapd.printf(`    * bw320_offset: ${req.args.bw320_offset}`);
+			hostapd.printf(`    * csa: ${req.args.csa}`);
+
+			let phy = req.args.phy;
+			let config = hostapd.data.config[phy];
+			if (!config || !config.bss || !config.bss[0] || !config.bss[0].ifname)
+				return 0;
+
+			let iface = hostapd.interfaces[phy];
+			if (!iface)
+				return 0;
+
+			if (!req.args.up) {
+				iface.stop();
+				return 0;
+			}
+
+			if (!req.args.frequency)
+				return libubus.STATUS_INVALID_ARGUMENT;
+
+			let freq_info = iface_freq_info(iface, config, req.args);
+			if (!freq_info)
+				return libubus.STATUS_UNKNOWN_ERROR;
+
+			let ret;
+			if (req.args.csa) {
+				freq_info.csa_count = req.args.csa_count ?? 10;
+				ret = iface.switch_channel(freq_info);
+			} else {
+				ret = iface.start(freq_info);
 			}
+			if (!ret)
+				return libubus.STATUS_UNKNOWN_ERROR;
 
 			return 0;
-		}
+		})
+	},
+	config_get_macaddr_list: {
+		args: {
+			phy: ""
+		},
+		call: ex_wrap(function(req) {
+			let phy = req.args.phy;
+			if (!phy)
+				return libubus.STATUS_INVALID_ARGUMENT;
+
+			let ret = {
+				macaddr: [],
+			};
+
+			let config = hostapd.data.config[phy];
+			if (!config)
+				return ret;
+
+			ret.macaddr = map(config.bss, (bss) => bss.bssid);
+			return ret;
+		})
 	},
 	config_set: {
 		args: {
@@ -311,7 +742,7 @@
 			config: "",
 			prev_config: "",
 		},
-		call: function(req) {
+		call: ex_wrap(function(req) {
 			let phy = req.args.phy;
 			let file = req.args.config;
 			let prev_file = req.args.prev_config;
@@ -319,34 +750,29 @@
 			if (!phy)
 				return libubus.STATUS_INVALID_ARGUMENT;
 
-			try {
-				if (prev_file && !hostapd.data.config[phy]) {
-					let config = iface_load_config(prev_file);
-					if (config)
-						config.radio.data = [];
-					hostapd.data.config[phy] = config;
-				}
+			if (prev_file && !hostapd.data.config[phy]) {
+				let config = iface_load_config(prev_file);
+				if (config)
+					config.radio.data = [];
+				hostapd.data.config[phy] = config;
+			}
 
-				let config = iface_load_config(file);
+			let config = iface_load_config(file);
 
-				hostapd.printf(`Set new config for phy ${phy}: ${file}`);
-				iface_set_config(phy, config);
-			} catch(e) {
-				hostapd.printf(`Error loading config: ${e}\n${e.stacktrace[0].context}`);
-				return libubus.STATUS_INVALID_ARGUMENT;
-			}
+			hostapd.printf(`Set new config for phy ${phy}: ${file}`);
+			iface_set_config(phy, config);
 
 			return {
 				pid: hostapd.getpid()
 			};
-		}
+		})
 	},
 	config_add: {
 		args: {
 			iface: "",
 			config: "",
 		},
-		call: function(req) {
+		call: ex_wrap(function(req) {
 			if (!req.args.iface || !req.args.config)
 				return libubus.STATUS_INVALID_ARGUMENT;
 
@@ -356,19 +782,19 @@
 			return {
 				pid: hostapd.getpid()
 			};
-		}
+		})
 	},
 	config_remove: {
 		args: {
 			iface: ""
 		},
-		call: function(req) {
+		call: ex_wrap(function(req) {
 			if (!req.args.iface)
 				return libubus.STATUS_INVALID_ARGUMENT;
 
 			hostapd.remove_iface(req.args.iface);
 			return 0;
-		}
+		})
 	},
 };
 
diff --git a/recipes-wifi/hostapd/files/wdev.uc b/recipes-wifi/hostapd/files/wdev.uc
index 5b32142..cf438f7 100644
--- a/recipes-wifi/hostapd/files/wdev.uc
+++ b/recipes-wifi/hostapd/files/wdev.uc
@@ -1,11 +1,14 @@
 #!/usr/bin/env ucode
 'use strict';
-import { vlist_new, is_equal, wdev_create, wdev_remove } from "/usr/share/hostap/common.uc";
+import { vlist_new, is_equal, wdev_create, wdev_remove, phy_open } from "/usr/share/hostap/common.uc";
 import { readfile, writefile, basename, readlink, glob } from "fs";
+let libubus = require("ubus");
 
 let keep_devices = {};
 let phy = shift(ARGV);
-let new_config = shift(ARGV);
+let command = shift(ARGV);
+let phydev;
+
 const mesh_params = [
 	"mesh_retry_timeout", "mesh_confirm_timeout", "mesh_holding_timeout", "mesh_max_peer_links",
 	"mesh_max_retries", "mesh_ttl", "mesh_element_ttl", "mesh_hwmp_max_preq_retries",
@@ -33,7 +36,12 @@
 		system([ "ip", "link", "set", "dev", ifname, "down" ]);
 		wdev_remove(ifname);
 	}
-	wdev_create(phy, ifname, wdev);
+	let wdev_config = {};
+	for (let key in wdev)
+		wdev_config[key] = wdev[key];
+	if (!wdev_config.macaddr && wdev.mode != "monitor")
+		wdev_config.macaddr = phydev.macaddr_next();
+	wdev_create(phy, ifname, wdev_config);
 	system([ "ip", "link", "set", "dev", ifname, "up" ]);
 	if (wdev.freq)
 		system(`iw dev ${ifname} set freq ${wdev.freq} ${wdev.htmode}`);
@@ -47,7 +55,7 @@
 		system(cmd);
 	} else if (wdev.mode == "mesh") {
 		let cmd = [ "iw", "dev", ifname, "mesh", "join", wdev.ssid, "freq", wdev.freq, wdev.htmode ];
-		for (let key in [ "beacon-interval", "mcast-rate" ])
+		for (let key in [ "mcast-rate", "beacon-interval" ])
 			if (wdev[key])
 				push(cmd, key, wdev[key]);
 		system(cmd);
@@ -114,43 +122,86 @@
 	}
 }
 
-
-let statefile = `/var/run/wdev-${phy}.json`;
-
-for (let dev in ARGV)
-	keep_devices[dev] = true;
+function usage()
+{
+	warn(`Usage: ${basename(sourcepath())} <phy> <command> [<arguments>]
 
-if (!phy || !new_config) {
-	warn(`Usage: ${basename(sourcepath())} <phy> <config> [<device]...]\n`);
+Commands:
+	set_config <config> [<device]...] - set phy configuration
+	get_macaddr <id>		  - get phy MAC address for vif index <id>
+`);
 	exit(1);
 }
 
-if (!readfile(`/sys/class/ieee80211/${phy}/index`)) {
-	warn(`PHY ${phy} does not exist\n`);
-	exit(1);
-}
+const commands = {
+	set_config: function(args) {
+		let statefile = `/var/run/wdev-${phy}.json`;
 
-new_config = json(new_config);
-if (!new_config) {
-	warn("Invalid configuration\n");
-	exit(1);
-}
+		let new_config = shift(args);
+		for (let dev in ARGV)
+			keep_devices[dev] = true;
 
-let old_config = readfile(statefile);
-if (old_config)
-	old_config = json(old_config);
+		if (!new_config)
+			usage();
 
-let config = vlist_new(iface_cb);
-if (type(old_config) == "object")
-	config.data = old_config;
+		new_config = json(new_config);
+		if (!new_config) {
+			warn("Invalid configuration\n");
+			exit(1);
+		}
 
-add_existing(phy, config.data);
-add_ifname(config.data);
-drop_inactive(config.data);
+		let old_config = readfile(statefile);
+		if (old_config)
+			old_config = json(old_config);
 
-add_ifname(new_config);
-config.update(new_config);
+		let config = vlist_new(iface_cb);
+		if (type(old_config) == "object")
+			config.data = old_config;
+
+		add_existing(phy, config.data);
+		add_ifname(config.data);
+		drop_inactive(config.data);
+
+		let ubus = libubus.connect();
+		let data = ubus.call("hostapd", "config_get_macaddr_list", { phy: phy });
+		let macaddr_list = [];
+		if (type(data) == "object" && data.macaddr)
+			macaddr_list = data.macaddr;
+		ubus.disconnect();
+		phydev.macaddr_init(macaddr_list);
+
+		add_ifname(new_config);
+		config.update(new_config);
+
+		drop_inactive(config.data);
+		delete_ifname(config.data);
+		writefile(statefile, sprintf("%J", config.data));
+	},
+	get_macaddr: function(args) {
+		let data = {};
+
+		for (let arg in args) {
+			arg = split(arg, "=", 2);
+			data[arg[0]] = arg[1];
+		}
+
+		let macaddr = phydev.macaddr_generate(data);
+		if (!macaddr) {
+			warn(`Could not get MAC address for phy ${phy}\n`);
+			exit(1);
+		}
+
+		print(macaddr + "\n");
+	},
+};
+
+if (!phy || !command | !commands[command])
+	usage();
+
+phydev = phy_open(phy);
+if (!phydev) {
+	warn(`PHY ${phy} does not exist\n`);
+	exit(1);
+}
 
-drop_inactive(config.data);
-delete_ifname(config.data);
-writefile(statefile, sprintf("%J", config.data));
+commands[command](ARGV);
diff --git a/recipes-wifi/hostapd/files/wpa_supplicant.uc b/recipes-wifi/hostapd/files/wpa_supplicant.uc
new file mode 100644
index 0000000..2a9de67
--- /dev/null
+++ b/recipes-wifi/hostapd/files/wpa_supplicant.uc
@@ -0,0 +1,335 @@
+let libubus = require("ubus");
+import { open, readfile } from "fs";
+import { wdev_create, wdev_remove, is_equal, vlist_new, phy_open } from "common";
+
+let ubus = libubus.connect();
+
+wpas.data.config = {};
+wpas.data.iface_phy = {};
+wpas.data.macaddr_list = {};
+
+function iface_stop(iface)
+{
+	let ifname = iface.config.iface;
+
+	if (!iface.running)
+		return;
+
+	delete wpas.data.iface_phy[ifname];
+	wpas.remove_iface(ifname);
+	wdev_remove(ifname);
+	iface.running = false;
+}
+
+function iface_start(phydev, iface, macaddr_list)
+{
+	let phy = phydev.name;
+
+	if (iface.running)
+		return;
+
+	let ifname = iface.config.iface;
+	let wdev_config = {};
+	for (let field in iface.config)
+		wdev_config[field] = iface.config[field];
+	if (!wdev_config.macaddr)
+		wdev_config.macaddr = phydev.macaddr_next();
+
+	wpas.data.iface_phy[ifname] = phy;
+	wdev_remove(ifname);
+	let ret = wdev_create(phy, ifname, wdev_config);
+	if (ret)
+		wpas.printf(`Failed to create device ${ifname}: ${ret}`);
+	wpas.add_iface(iface.config);
+	iface.running = true;
+}
+
+function iface_cb(new_if, old_if)
+{
+	if (old_if && new_if && is_equal(old_if.config, new_if.config)) {
+		new_if.running = old_if.running;
+		return;
+	}
+
+	if (new_if && old_if)
+		wpas.printf(`Update configuration for interface ${old_if.config.iface}`);
+	else if (old_if)
+		wpas.printf(`Remove interface ${old_if.config.iface}`);
+
+	if (old_if)
+		iface_stop(old_if);
+}
+
+function prepare_config(config)
+{
+	config.config_data = readfile(config.config);
+
+	return { config: config };
+}
+
+function set_config(phy_name, config_list)
+{
+	let phy = wpas.data.config[phy_name];
+
+	if (!phy) {
+		phy = vlist_new(iface_cb, false);
+		wpas.data.config[phy_name] = phy;
+	}
+
+	let values = [];
+	for (let config in config_list)
+		push(values, [ config.iface, prepare_config(config) ]);
+
+	phy.update(values);
+}
+
+function start_pending(phy_name)
+{
+	let phy = wpas.data.config[phy_name];
+	let ubus = wpas.data.ubus;
+
+	if (!phy || !phy.data)
+		return;
+
+	let phydev = phy_open(phy_name);
+	if (!phydev) {
+		wpas.printf(`Could not open phy ${phy_name}`);
+		return;
+	}
+
+	let macaddr_list = wpas.data.macaddr_list[phy_name];
+	phydev.macaddr_init(macaddr_list);
+
+	for (let ifname in phy.data)
+		iface_start(phydev, phy.data[ifname]);
+}
+
+let main_obj = {
+	phy_set_state: {
+		args: {
+			phy: "",
+			stop: true,
+		},
+		call: function(req) {
+			if (!req.args.phy || req.args.stop == null)
+				return libubus.STATUS_INVALID_ARGUMENT;
+
+			let phy = wpas.data.config[req.args.phy];
+			if (!phy)
+				return libubus.STATUS_NOT_FOUND;
+
+			try {
+				if (req.args.stop) {
+					for (let ifname in phy.data)
+						iface_stop(phy.data[ifname]);
+				} else {
+					start_pending(req.args.phy);
+				}
+			} catch (e) {
+				wpas.printf(`Error chaging state: ${e}\n${e.stacktrace[0].context}`);
+				return libubus.STATUS_INVALID_ARGUMENT;
+			}
+			return 0;
+		}
+	},
+	phy_set_macaddr_list: {
+		args: {
+			phy: "",
+			macaddr: [],
+		},
+		call: function(req) {
+			let phy = req.args.phy;
+			if (!phy)
+				return libubus.STATUS_INVALID_ARGUMENT;
+
+			wpas.data.macaddr_list[phy] = req.args.macaddr;
+			return 0;
+		}
+	},
+	phy_status: {
+		args: {
+			phy: ""
+		},
+		call: function(req) {
+			if (!req.args.phy)
+				return libubus.STATUS_INVALID_ARGUMENT;
+
+			let phy = wpas.data.config[req.args.phy];
+			if (!phy)
+				return libubus.STATUS_NOT_FOUND;
+
+			for (let ifname in phy.data) {
+				try {
+					let iface = wpas.interfaces[ifname];
+					if (!iface)
+						continue;
+
+					let status = iface.status();
+					if (!status)
+						continue;
+
+					if (status.state == "INTERFACE_DISABLED")
+						continue;
+
+					status.ifname = ifname;
+					return status;
+				} catch (e) {
+					continue;
+				}
+			}
+
+			return libubus.STATUS_NOT_FOUND;
+		}
+	},
+	config_set: {
+		args: {
+			phy: "",
+			config: [],
+			defer: true,
+		},
+		call: function(req) {
+			if (!req.args.phy)
+				return libubus.STATUS_INVALID_ARGUMENT;
+
+			wpas.printf(`Set new config for phy ${req.args.phy}`);
+			try {
+				if (req.args.config)
+					set_config(req.args.phy, req.args.config);
+
+				if (!req.args.defer)
+					start_pending(req.args.phy);
+			} catch (e) {
+				wpas.printf(`Error loading config: ${e}\n${e.stacktrace[0].context}`);
+				return libubus.STATUS_INVALID_ARGUMENT;
+			}
+
+			return {
+				pid: wpas.getpid()
+			};
+		}
+	},
+	config_add: {
+		args: {
+			driver: "",
+			iface: "",
+			bridge: "",
+			hostapd_ctrl: "",
+			ctrl: "",
+			config: "",
+		},
+		call: function(req) {
+			if (!req.args.iface || !req.args.config)
+				return libubus.STATUS_INVALID_ARGUMENT;
+
+			if (wpas.add_iface(req.args) < 0)
+				return libubus.STATUS_INVALID_ARGUMENT;
+
+			return {
+				pid: wpas.getpid()
+			};
+		}
+	},
+	config_remove: {
+		args: {
+			iface: ""
+		},
+		call: function(req) {
+			if (!req.args.iface)
+				return libubus.STATUS_INVALID_ARGUMENT;
+
+			wpas.remove_iface(req.args.iface);
+			return 0;
+		}
+	},
+};
+
+wpas.data.ubus = ubus;
+wpas.data.obj = ubus.publish("wpa_supplicant", main_obj);
+
+function iface_event(type, name, data) {
+	let ubus = wpas.data.ubus;
+
+	data ??= {};
+	data.name = name;
+	wpas.data.obj.notify(`iface.${type}`, data, null, null, null, -1);
+	ubus.call("service", "event", { type: `wpa_supplicant.${name}.${type}`, data: {} });
+}
+
+function iface_hostapd_notify(phy, ifname, iface, state)
+{
+	let ubus = wpas.data.ubus;
+	let status = iface.status();
+	let msg = { phy: phy };
+
+	wpas.printf(`ucode: mtk: wpa_s in state ${state} notifies hostapd`);
+	switch (state) {
+	case "DISCONNECTED":
+	case "AUTHENTICATING":
+	case "SCANNING":
+		msg.up = false;
+		break;
+	case "INTERFACE_DISABLED":
+	case "INACTIVE":
+		msg.up = true;
+		break;
+	case "COMPLETED":
+		msg.up = true;
+		msg.frequency = status.frequency;
+		msg.sec_chan_offset = status.sec_chan_offset;
+		msg.ch_width = status.ch_width;
+		msg.bw320_offset = status.bw320_offset;
+		break;
+	default:
+		return;
+	}
+
+	ubus.call("hostapd", "apsta_state", msg);
+}
+
+function iface_channel_switch(phy, ifname, iface, info)
+{
+	let msg = {
+		phy: phy,
+		up: true,
+		csa: true,
+		csa_count: info.csa_count ? info.csa_count - 1 : 0,
+		frequency: info.frequency,
+		ch_width: info.ch_width,
+		bw320_offset: info.bw320_offset,
+		sec_chan_offset: info.sec_chan_offset,
+	};
+	ubus.call("hostapd", "apsta_state", msg);
+}
+
+return {
+	shutdown: function() {
+		for (let phy in wpas.data.config)
+			set_config(phy, []);
+		wpas.ubus.disconnect();
+	},
+	iface_add: function(name, obj) {
+		iface_event("add", name);
+	},
+	iface_remove: function(name, obj) {
+		iface_event("remove", name);
+	},
+	state: function(ifname, iface, state) {
+		let phy = wpas.data.iface_phy[ifname];
+		if (!phy) {
+			wpas.printf(`no PHY for ifname ${ifname}`);
+			return;
+		}
+
+		iface_hostapd_notify(phy, ifname, iface, state);
+	},
+	event: function(ifname, iface, ev, info) {
+		let phy = wpas.data.iface_phy[ifname];
+		if (!phy) {
+			wpas.printf(`no PHY for ifname ${ifname}`);
+			return;
+		}
+
+		if (ev == "CH_SWITCH_STARTED")
+			iface_channel_switch(phy, ifname, iface, info);
+	}
+};