added dynamic radio assignment #9

Merged
Koda merged 1 commits from radio-assignment into main 2026-07-12 13:40:07 +01:00
6 changed files with 248 additions and 22 deletions
Showing only changes of commit f129fa53cf - Show all commits
+1
View File
@@ -3,3 +3,4 @@ CONFIG_TARGET_mpc85xx_p1020=y
CONFIG_TARGET_mpc85xx_p1020_DEVICE_hpe_msm460=y
CONFIG_PACKAGE_kmod-tun=m
CONFIG_PACKAGE_openuf=m
CONFIG_PACKAGE_usteer=m
+1 -1
View File
@@ -13,7 +13,7 @@ define Package/openuf
SECTION := net
CATEGORY := Network
TITLE := openUF — UniFi bridge daemon for OpenWrt
DEPENDS := +libmbedtls +libuci +libjson-c +kmod-tun
DEPENDS := +libmbedtls +libuci +libjson-c +kmod-tun +usteer
URL := https://git.ascheu.de/Koda/OpenUniFi
endef
+2 -2
View File
@@ -1,4 +1,4 @@
# openUF — C
# openUF
Daemon that makes an OpenWrt router appear as a UniFi AP to UniFi Network controllers.
@@ -79,7 +79,7 @@ enable_inform = 1 # Enable logging to /var/log/openuf.log if set t
---
## Glassary
## Glossary
### TNBU
+7 -2
View File
@@ -228,11 +228,13 @@ static struct json_object *build_radio_table_stats(const uf_model_t *m)
for (int i = 0; i < m->radio_map_len; i++) {
const uf_radio_map_t *rm = &m->radio_map[i];
const char *device = wlan_device_for_band(m, rm->band);
if (!device) device = rm->device;
/* Map "radio0" → "wlan0" by OpenWrt convention */
char wlan_iface[32];
int ridx = 0;
sscanf(rm->device, "radio%d", &ridx);
sscanf(device, "radio%d", &ridx);
snprintf(wlan_iface, sizeof(wlan_iface), "wlan%d", ridx);
/* Radio name in the static table */
@@ -388,7 +390,10 @@ static struct json_object *build_vap_table(const uf_model_t *m)
for (int j = 0; j < m->radio_map_len; j++) {
if (strcmp(m->radio_map[j].band, radio) == 0) {
int idx = 0;
sscanf(m->radio_map[j].device, "radio%d", &idx);
const char *device = wlan_device_for_band(
m, m->radio_map[j].band);
if (!device) device = m->radio_map[j].device;
sscanf(device, "radio%d", &idx);
if (!ifname || !ifname[0])
snprintf(wlan_iface, sizeof(wlan_iface), "phy%d-ap0", idx);
radio_stats_t rs;
+231 -16
View File
@@ -82,6 +82,147 @@
#include "wlan.h"
#include "ufmodel.h"
#define MAX_RESOLVED_RADIOS 8
typedef struct {
char device[32];
unsigned int bands;
} radio_capability_t;
static const uf_model_t *resolved_model;
static char resolved_devices[MAX_RESOLVED_RADIOS][32];
static unsigned int band_bit(const char *band)
{
if (!band) return 0;
if (!strcmp(band, "ng") || !strcmp(band, "2g")) return 1u;
if (!strcmp(band, "na") || !strcmp(band, "5g")) return 2u;
if (!strcmp(band, "6g") || !strcmp(band, "6GHz")) return 4u;
return 0;
}
static int bit_count(unsigned int value)
{
int count = 0;
while (value) {
count += value & 1u;
value >>= 1;
}
return count;
}
/* OpenWrt's generated radioN and phyN indices correspond for mac80211
* devices. Read actual frequencies instead of assuming PHY band order. */
static unsigned int detect_radio_bands(const char *device)
{
int phy_index = -1;
char command[96];
char line[256];
unsigned int bands = 0;
if (!device || sscanf(device, "radio%d", &phy_index) != 1 || phy_index < 0)
return 0;
snprintf(command, sizeof(command), "iw phy phy%d info 2>/dev/null", phy_index);
FILE *pipe = popen(command, "r");
if (!pipe) return 0;
while (fgets(line, sizeof(line), pipe)) {
char *mhz = strstr(line, " MHz [");
if (!mhz || strstr(line, "(disabled)"))
continue;
char *start = mhz;
while (start > line &&
((start[-1] >= '0' && start[-1] <= '9') || start[-1] == '.'))
start--;
double frequency = strtod(start, NULL);
if (frequency >= 2300.0 && frequency < 3000.0)
bands |= 1u;
else if (frequency >= 4900.0 && frequency < 5925.0)
bands |= 2u;
else if (frequency >= 5925.0 && frequency < 7200.0)
bands |= 4u;
}
pclose(pipe);
return bands;
}
static void resolve_radio_map(const uf_model_t *model)
{
if (!model || resolved_model == model)
return;
memset(resolved_devices, 0, sizeof(resolved_devices));
resolved_model = model;
int count = model->radio_map_len;
if (count > MAX_RESOLVED_RADIOS)
count = MAX_RESOLVED_RADIOS;
radio_capability_t caps[MAX_RESOLVED_RADIOS] = {0};
int used[MAX_RESOLVED_RADIOS] = {0};
for (int i = 0; i < count; i++) {
snprintf(caps[i].device, sizeof(caps[i].device), "%s",
model->radio_map[i].device);
caps[i].bands = detect_radio_bands(caps[i].device);
}
for (int i = 0; i < count; i++) {
unsigned int wanted = band_bit(model->radio_map[i].band);
int best = -1;
int best_band_count = 99;
for (int j = 0; j < count; j++) {
if (used[j] || !(caps[j].bands & wanted))
continue;
int supported = bit_count(caps[j].bands);
if (supported < best_band_count) {
best = j;
best_band_count = supported;
}
}
if (best >= 0) {
used[best] = 1;
snprintf(resolved_devices[i], sizeof(resolved_devices[i]), "%s",
caps[best].device);
} else {
snprintf(resolved_devices[i], sizeof(resolved_devices[i]), "%s",
model->radio_map[i].device);
}
printf("[openuf] Radio mapping: %s -> %s%s\n",
model->radio_map[i].band, resolved_devices[i],
best >= 0 ? " (detected)" : " (model fallback)");
}
}
const char *wlan_device_for_band(const uf_model_t *model, const char *band)
{
if (!model || !band) return NULL;
resolve_radio_map(model);
for (int i = 0; i < model->radio_map_len; i++)
if (!strcmp(model->radio_map[i].band, band))
return i < MAX_RESOLVED_RADIOS && resolved_devices[i][0]
? resolved_devices[i] : model->radio_map[i].device;
return NULL;
}
const char *wlan_band_for_device(const uf_model_t *model, const char *device)
{
if (!model || !device) return NULL;
resolve_radio_map(model);
for (int i = 0; i < model->radio_map_len; i++) {
const char *mapped = i < MAX_RESOLVED_RADIOS && resolved_devices[i][0]
? resolved_devices[i]
: model->radio_map[i].device;
if (!strcmp(mapped, device))
return model->radio_map[i].band;
}
return NULL;
}
/* ─── Mapeo de seguridad UniFi → OpenWrt UCI ────────────────────── */
static const char *sec_to_uci(const char *uf)
{
@@ -430,6 +571,68 @@ static int ensure_vlan_network(int vid)
return ok ? 0 : -1;
}
/*
* Configure OpenWrt's steering policy engine. The hostapd 802.11k/v flags
* only expose measurements and transition commands; they do not decide when
* a station should move. usteer supplies that missing policy loop.
*/
static int configure_band_steering(int enabled)
{
struct uci_context *ctx = uci_alloc_context();
if (!ctx)
return -1;
struct uci_package *pkg = NULL;
if (uci_load(ctx, "usteer", &pkg) != UCI_OK) {
printf("[openuf] Cannot load /etc/config/usteer\n");
uci_free_context(ctx);
return -1;
}
struct uci_section *settings = NULL;
struct uci_element *element;
uci_foreach_element(&pkg->sections, element) {
struct uci_section *section = uci_to_section(element);
if (!strcmp(section->type, "usteer")) {
settings = section;
break;
}
}
if (!settings) {
if (uci_ensure_section(ctx, pkg, "openuf", "usteer") != 0) {
uci_unload(ctx, pkg);
uci_free_context(ctx);
return -1;
}
settings = uci_lookup_section(ctx, pkg, "openuf");
}
if (!settings) {
uci_unload(ctx, pkg);
uci_free_context(ctx);
return -1;
}
/*
* A zero interval disables higher-band steering. A zero station-count
* threshold is important for small networks: usteer's default of five
* otherwise prevents a lone client from being considered. The signal
* floor avoids pushing a client onto 5 GHz when that link is too weak.
*/
UCI_SET(ctx, "usteer", settings->e.name, "band_steering_interval",
enabled ? "30000" : "0");
UCI_SET(ctx, "usteer", settings->e.name, "band_steering_threshold", "0");
UCI_SET(ctx, "usteer", settings->e.name, "band_steering_min_snr", "-65");
int ok = uci_commit(ctx, &pkg, false) == UCI_OK;
uci_unload(ctx, pkg);
uci_free_context(ctx);
printf("[openuf] Band steering policy %s (usteer)\n",
enabled ? "enabled" : "disabled");
return ok ? 0 : -1;
}
/* ═══════════════════════════════════════════════════════════════════
wlan_clear — remove all VAPs before applying controller ownership
═══════════════════════════════════════════════════════════════════ */
@@ -836,12 +1039,11 @@ int wlan_apply_config(struct json_object *config_json,
const char *radio_band = "";
if (json_object_object_get_ex(r, "radio", &v))
radio_band = json_object_get_string(v);
const char *device_name = "radio0";
for (int j = 0; j < model->radio_map_len; j++) {
if (!strcmp(model->radio_map[j].band, radio_band)) {
device_name = model->radio_map[j].device;
break;
}
const char *device_name = wlan_device_for_band(model, radio_band);
if (!device_name) {
printf("[openuf] Ignoring settings for unknown radio '%s'\n",
radio_band);
continue;
}
wlan_apply_radio(r, device_name);
}
@@ -886,13 +1088,22 @@ int wlan_apply_config(struct json_object *config_json,
printf("[openuf] Disabled %d default OpenWrt VAPs\n",
disabled_defaults);
/* 3. Create VAPs */
/* 3. Create VAPs and determine whether any WLAN requests steering. */
int band_steering_enabled = 0;
if (vt_arr && json_object_is_type(vt_arr, json_type_array)) {
int nv = json_object_array_length(vt_arr);
for (int i = 0; i < nv; i++) {
struct json_object *vap = json_object_array_get_idx(vt_arr, i);
if (!vap) continue;
const char *band_steer_keys[] = {
"band_steering", "band_steering_enabled", "steering_enabled"
};
if (json_boolean_any(vap, band_steer_keys,
sizeof(band_steer_keys) /
sizeof(band_steer_keys[0])))
band_steering_enabled = 1;
/* A VAP without an explicit band is a model-wide WLAN. */
const char *radio_band = NULL;
if (json_object_object_get_ex(vap, "radio", &v))
@@ -909,7 +1120,9 @@ int wlan_apply_config(struct json_object *config_json,
strcmp(model->radio_map[j].band, radio_band))
continue;
int section_idx = i * model->radio_map_len + j;
if (apply_vap(ctx, pkg, vap, model->radio_map[j].device,
const char *device = wlan_device_for_band(
model, model->radio_map[j].band);
if (!device || apply_vap(ctx, pkg, vap, device,
model->radio_map[j].band,
mac_str, section_idx) != 0) {
uci_unload(ctx, pkg);
@@ -942,6 +1155,9 @@ int wlan_apply_config(struct json_object *config_json,
uci_unload(ctx, pkg);
uci_free_context(ctx);
if (configure_band_steering(band_steering_enabled) != 0)
printf("[openuf] Failed to configure the band steering policy\n");
/*
* Reload netifd for generated VLAN devices, then bring the radios up one
* at a time. Some dual-ath9k devices intermittently fail their first beacon
@@ -952,7 +1168,8 @@ int wlan_apply_config(struct json_object *config_json,
system("ubus call network reload >/dev/null 2>&1");
for (int i = 0; i < model->radio_map_len; i++) {
char command[256];
const char *device = model->radio_map[i].device;
const char *device = wlan_device_for_band(
model, model->radio_map[i].band);
/* Model radio names are internal constants, but validate defensively. */
if (!device ||
@@ -997,6 +1214,9 @@ int wlan_apply_config(struct json_object *config_json,
if (!radio_up)
printf("[openuf] %s failed after 2 start attempts\n", device);
}
/* Restart after hostapd has registered both BSSes on ubus. */
system("/etc/init.d/usteer restart >/dev/null 2>&1");
return 0;
}
@@ -1271,13 +1491,8 @@ struct json_object *wlan_get_vap_table(const uf_model_t *model)
if (!device) device = "radio0";
/* Band of this radio */
const char *radio_band = "ng";
for (int j = 0; j < model->radio_map_len; j++) {
if (!strcmp(model->radio_map[j].device, device)) {
radio_band = model->radio_map[j].band;
break;
}
}
const char *radio_band = wlan_band_for_device(model, device);
if (!radio_band) radio_band = "ng";
/* Resolve the actual netifd interface (for example phy1-ap0). */
char wlan_iface[32];
+5
View File
@@ -19,6 +19,11 @@ void wlan_clear(void);
void wlan_apply_radio(struct json_object *radio_json,
const char *device_name);
/* Resolve the model's logical UniFi band against the bands advertised by
* the local PHYs. Falls back to the model mapping when discovery fails. */
const char *wlan_device_for_band(const uf_model_t *model, const char *band);
const char *wlan_band_for_device(const uf_model_t *model, const char *device);
/* Apply full config pushed by controller (setstate).
* config_json: decoded setstate JSON object
* model : model descriptor for radio_map lookup */