From 37c286e7560227ccb2f33ff1a9c13faa33d224df Mon Sep 17 00:00:00 2001 From: Koda YeenBean Date: Tue, 14 Jul 2026 23:38:46 +0100 Subject: [PATCH] enable wifi version steering (#28) Reviewed-on: https://git.ascheu.de/Koda/OpenUniFi/pulls/28 --- src/state.h | 2 +- src/wlan.c | 153 ++++++++++++++++++++++++++++++++++++++++++++++------ src/wlan.h | 6 ++- 3 files changed, 141 insertions(+), 20 deletions(-) diff --git a/src/state.h b/src/state.h index 22af7c0..cf08a9b 100644 --- a/src/state.h +++ b/src/state.h @@ -3,7 +3,7 @@ #include -#define OPENUF_CONFIG_SCHEMA 6 +#define OPENUF_CONFIG_SCHEMA 7 typedef struct { bool adopted; diff --git a/src/wlan.c b/src/wlan.c index 6c278b2..15e05d1 100644 --- a/src/wlan.c +++ b/src/wlan.c @@ -737,6 +737,84 @@ void wlan_clear(void) uci_free_context(ctx); } +enum wifi_standard { + WIFI_STANDARD_UNKNOWN = 0, + WIFI_STANDARD_4 = 4, + WIFI_STANDARD_5 = 5, + WIFI_STANDARD_6 = 6, + WIFI_STANDARD_7 = 7, +}; + +/* Return the newest 802.11 generation advertised by the local PHY. */ +static enum wifi_standard radio_max_standard(const char *device_name) +{ + int phy_index = -1; + char command[96]; + char line[256]; + enum wifi_standard standard = WIFI_STANDARD_UNKNOWN; + + if (!device_name || sscanf(device_name, "radio%d", &phy_index) != 1 || + phy_index < 0) + return WIFI_STANDARD_UNKNOWN; + + snprintf(command, sizeof(command), "iw phy phy%d info 2>/dev/null", + phy_index); + FILE *pipe = popen(command, "r"); + if (!pipe) + return WIFI_STANDARD_UNKNOWN; + + while (fgets(line, sizeof(line), pipe)) { + if (strstr(line, "EHT Iftypes")) + standard = WIFI_STANDARD_7; + else if (standard < WIFI_STANDARD_6 && strstr(line, "HE Iftypes")) + standard = WIFI_STANDARD_6; + else if (standard < WIFI_STANDARD_5 && + strstr(line, "VHT Capabilities")) + standard = WIFI_STANDARD_5; + else if (standard < WIFI_STANDARD_4 && strstr(line, "Capabilities:")) + standard = WIFI_STANDARD_4; + } + pclose(pipe); + return standard; +} + +/* Keep the controller-selected width but use the newest PHY generation. */ +static void select_htmode(const char *device_name, const char *radio_band, + const char *requested, int force_wifi4, + char *result, size_t result_size) +{ + enum wifi_standard standard = radio_max_standard(device_name); + int width = 20; + const char *number = requested; + + while (number && *number && (*number < '0' || *number > '9')) + number++; + if (number && *number) + width = atoi(number); + + if (force_wifi4) { + /* 802.11n cannot use 80 MHz or wider channels. */ + standard = WIFI_STANDARD_4; + if (width > 40) + width = 40; + } else if (standard == WIFI_STANDARD_5 && radio_band && + (!strcmp(radio_band, "ng") || !strcmp(radio_band, "2g"))) { + /* OpenWrt does not use VHT modes on the 2.4 GHz band. */ + standard = WIFI_STANDARD_4; + } + + const char *prefix = standard == WIFI_STANDARD_7 ? "EHT" : + standard == WIFI_STANDARD_6 ? "HE" : + standard == WIFI_STANDARD_5 ? "VHT" : + standard == WIFI_STANDARD_4 ? "HT" : NULL; + if (!prefix) { + snprintf(result, result_size, "%s", requested ? requested : "HT20"); + return; + } + + snprintf(result, result_size, "%s%d", prefix, width); +} + /* ═══════════════════════════════════════════════════════════════════ wlan_apply_radio — apply radio config (channel, HT, power) ═══════════════════════════════════════════════════════════════════ @@ -748,7 +826,8 @@ void wlan_clear(void) min_rssi → not mapped to UCI (requires an external daemon) */ void wlan_apply_radio(struct json_object *radio_json, - const char *device_name) + const char *device_name, + int force_wifi4) { struct uci_context *ctx = uci_alloc_context(); if (!ctx) return; @@ -762,11 +841,12 @@ void wlan_apply_radio(struct json_object *radio_json, char path[256]; /* Map UniFi band names to OpenWrt mac80211 band names. */ + const char *radio_band = NULL; if (json_object_object_get_ex(radio_json, "radio", &v)) { - const char *radio = json_object_get_string(v); - const char *band = !strcmp(radio, "ng") ? "2g" : - !strcmp(radio, "na") ? "5g" : - !strcmp(radio, "6g") ? "6g" : NULL; + radio_band = json_object_get_string(v); + const char *band = !strcmp(radio_band, "ng") ? "2g" : + !strcmp(radio_band, "na") ? "5g" : + !strcmp(radio_band, "6g") ? "6g" : NULL; if (band) { snprintf(path, sizeof(path), "wireless.%s.band=%s", device_name, band); @@ -776,17 +856,20 @@ void wlan_apply_radio(struct json_object *radio_json, } } -#define RP(key, uci_opt) \ - if (json_object_object_get_ex(radio_json, key, &v)) { \ - snprintf(path, sizeof(path), "wireless.%s.%s=%s", \ - device_name, uci_opt, json_object_get_string(v)); \ - struct uci_ptr ptr; \ - if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK) \ - uci_set(ctx, &ptr); \ + if (json_object_object_get_ex(radio_json, "ht", &v)) { + const char *requested = json_object_get_string(v); + char htmode[16]; + select_htmode(device_name, radio_band, requested, force_wifi4, + htmode, sizeof(htmode)); + snprintf(path, sizeof(path), "wireless.%s.htmode=%s", + device_name, htmode); + struct uci_ptr ptr; + if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK) + uci_set(ctx, &ptr); + printf("[openuf] Radio %s standard: %s%s\n", device_name, htmode, + force_wifi4 ? " (Force WiFi 4)" : " (newest supported)"); } - RP("ht", "htmode"); - /* Channel: 0 = auto in UniFi */ if (json_object_object_get_ex(radio_json, "channel", &v)) { int ch = json_object_get_int(v); @@ -815,8 +898,6 @@ void wlan_apply_radio(struct json_object *radio_json, if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK) uci_set(ctx, &ptr); -#undef RP - uci_commit(ctx, &pkg, false); uci_unload(ctx, pkg); uci_free_context(ctx); @@ -1110,6 +1191,37 @@ static int apply_vap(struct uci_context *ctx, return 0; } +/* True when a UniFi WLAN on this band explicitly requests WiFi 4 mode. */ +static int force_wifi4_for_band(struct json_object *vaps, const char *band) +{ + if (!vaps || !json_object_is_type(vaps, json_type_array)) + return 0; + + const char *force_keys[] = { + "iot", "force_wifi4", "force_wifi4_mode" + }; + int count = json_object_array_length(vaps); + for (int i = 0; i < count; i++) { + struct json_object *vap = json_object_array_get_idx(vaps, i); + struct json_object *value; + if (!vap || !json_boolean_any(vap, force_keys, + sizeof(force_keys) / + sizeof(force_keys[0]))) + continue; + + const char *vap_band = NULL; + if (json_object_object_get_ex(vap, "radio", &value)) + vap_band = json_object_get_string(value); + if (!vap_band || !vap_band[0] || !strcmp(vap_band, "both") || + !strcmp(vap_band, "all") || !strcmp(vap_band, band) || + (!strcmp(vap_band, "2g") && !strcmp(band, "ng")) || + (!strcmp(vap_band, "5g") && !strcmp(band, "na")) || + (!strcmp(vap_band, "6GHz") && !strcmp(band, "6g"))) + return 1; + } + return 0; +} + /* ═══════════════════════════════════════════════════════════════════ wlan_apply_config — apply the controller's full configuration ═══════════════════════════════════════════════════════════════════ @@ -1167,7 +1279,8 @@ int wlan_apply_config(struct json_object *config_json, radio_band); continue; } - wlan_apply_radio(r, device_name); + wlan_apply_radio(r, device_name, + force_wifi4_for_band(vt_arr, radio_band)); } } @@ -1476,6 +1589,12 @@ int wlan_apply_system_cfg(const char *system_cfg, band = "na"; json_object_object_add(vap, "radio", json_object_new_string(band)); + /* UniFi's legacy Force WiFi 4 mode is carried as wireless.N.iot. */ + snprintf(key, sizeof(key), "wireless.%d.iot", i); + if (system_cfg_get(system_cfg, key, value, sizeof(value))) + json_object_object_add(vap, "iot", json_object_new_boolean( + feature_text_enabled(value))); + char passphrase[256] = {0}; snprintf(key, sizeof(key), "aaa.%d.wpa.psk", i); int has_passphrase = system_cfg_get(system_cfg, key, passphrase, diff --git a/src/wlan.h b/src/wlan.h index 33de5d4..d939209 100644 --- a/src/wlan.h +++ b/src/wlan.h @@ -15,9 +15,11 @@ void wlan_clear(void); /* Apply radio-level settings from a UniFi radio_table entry. * radio_json : JSON object with fields: channel, ht, tx_power - * device_name: OpenWrt radio device ("radio0", "radio1") */ + * device_name: OpenWrt radio device ("radio0", "radio1") + * force_wifi4: cap the radio at 802.11n when requested by UniFi */ void wlan_apply_radio(struct json_object *radio_json, - const char *device_name); + const char *device_name, + int force_wifi4); /* Resolve the model's logical UniFi band against the bands advertised by * the local PHYs. Falls back to the model mapping when discovery fails. */