[PATCH 02/15] AP: Set the RNR 20 MHz PSD subfield for co-located 6 GHz APs

Andrei-Alexandru Bleortu me at andrei-z.com
Sun Oct 4 03:41:55 PDT 2026


IEEE Std 802.11-2024, 11.49 requires an AP that reports a co-located
6 GHz AP in its Reduced Neighbor Report element to set the 20 MHz PSD
subfield so that the resulting maximum transmit power on the primary
20 MHz channel equals the minimum of the regulatory client limits in
the 6 GHz AP's Transmit Power Envelope elements. hostapd always wrote
127 ("no maximum specified"), so a client probing the 6 GHz channel
before receiving its beacon had no limit to apply.

Derive the value from the regulatory client EIRP PSD limits the 6 GHz AP
advertises (from AFC when available, else the configured or built-in
Default and, for an indoor AP, Subordinate values), lowered by its
regulatory client EIRP, if any, spread over 20 MHz. The configured or
default Default category PSD now comes from one helper shared with the
Transmit Power Envelope element.

Assisted-by: LLM
Signed-off-by: Andrei-Alexandru Bleortu <me at andrei-z.com>
---
 src/ap/ieee802_11.c    | 68 ++++++++++++++++++++++++++++++++++++------
 tests/hwsim/test_he.py | 49 ++++++++++++++++++++++++++++++
 2 files changed, 108 insertions(+), 9 deletions(-)

diff --git a/src/ap/ieee802_11.c b/src/ap/ieee802_11.c
index 219a94b0f..8243cfaac 100644
--- a/src/ap/ieee802_11.c
+++ b/src/ap/ieee802_11.c
@@ -9946,6 +9946,17 @@ static u8 * hostapd_add_tpe_info(u8 *eid, u8 tx_pwr_count,
 #define REG_PSD_MAX_TXPOWER_FOR_DEFAULT_CLIENT      (-1) /* dBm/MHz */
 #define REG_PSD_MAX_TXPOWER_FOR_SUBORDINATE_CLIENT  5    /* dBm/MHz */
 
+#ifdef CONFIG_IEEE80211AX
+/* Regulatory client EIRP PSD for the Default category, in units of 0.5 dBm/MHz,
+ * when AFC does not provide one */
+static int hostapd_reg_def_cli_eirp_psd(struct hostapd_config *iconf)
+{
+	if (iconf->reg_def_cli_eirp_psd != -1)
+		return iconf->reg_def_cli_eirp_psd;
+	return REG_PSD_MAX_TXPOWER_FOR_DEFAULT_CLIENT * 2;
+}
+#endif /* CONFIG_IEEE80211AX */
+
 u8 * hostapd_eid_txpower_envelope(struct hostapd_data *hapd, u8 *eid)
 {
 	struct hostapd_iface *iface = hapd->iface;
@@ -9993,14 +10004,8 @@ u8 * hostapd_eid_txpower_envelope(struct hostapd_data *hapd, u8 *eid)
 		/* Default Transmit Power Envelope for Global Operating Class */
 		err = hostap_afc_get_chan_max_eirp_power(iface, true,
 							 &max_eirp_psd);
-		if (err < 0) {
-			if (hapd->iconf->reg_def_cli_eirp_psd != -1)
-				max_eirp_psd =
-					hapd->iconf->reg_def_cli_eirp_psd;
-			else
-				max_eirp_psd =
-					REG_PSD_MAX_TXPOWER_FOR_DEFAULT_CLIENT * 2;
-		}
+		if (err < 0)
+			max_eirp_psd = hostapd_reg_def_cli_eirp_psd(iconf);
 
 		eid = hostapd_add_tpe_info(eid, tx_pwr_count, tx_pwr_intrpn,
 					   REG_DEFAULT_CLIENT, max_eirp_psd);
@@ -10806,6 +10811,50 @@ static u8 * hostapd_eid_nr_db(struct hostapd_data *hapd, u8 *eid,
 }
 
 
+/*
+ * 20 MHz PSD subfield for a reported 6 GHz AP: the lowest regulatory client
+ * limit in that AP's Transmit Power Envelope elements (IEEE Std 802.11-2024,
+ * 11.49), in units of 0.5 dBm/MHz.
+ */
+static u8 hostapd_rnr_6ghz_psd(struct hostapd_data *bss)
+{
+#ifdef CONFIG_IEEE80211AX
+	struct hostapd_iface *iface = bss->iface;
+	int psd, eirp;
+
+	if (hostap_afc_get_chan_max_eirp_power(iface, true, &psd) < 0) {
+		psd = hostapd_reg_def_cli_eirp_psd(iface->conf);
+		/* An indoor AP also advertises the subordinate client limit */
+		if (he_reg_is_indoor(iface->conf->he_6ghz_reg_pwr_type)) {
+			int sub = iface->conf->reg_sub_cli_eirp_psd != -1 ?
+				iface->conf->reg_sub_cli_eirp_psd :
+				REG_PSD_MAX_TXPOWER_FOR_SUBORDINATE_CLIENT * 2;
+
+			if (sub < psd)
+				psd = sub;
+		}
+	}
+
+	if (hostap_afc_get_chan_max_eirp_power(iface, false, &eirp)) {
+		eirp = iface->conf->reg_def_cli_eirp;
+		if (!he_reg_is_sp(iface->conf->he_6ghz_reg_pwr_type))
+			eirp = -1;
+	}
+	/* An EIRP limit spread over 20 MHz is a PSD limit 13 dB lower */
+	if (eirp != -1 && eirp - 26 < psd)
+		psd = eirp - 26;
+
+	if (psd < -127)
+		psd = -127;
+	else if (psd >= RNR_20_MHZ_PSD_MAX_TXPOWER)
+		psd = RNR_20_MHZ_PSD_MAX_TXPOWER - 1;
+	return (u8) psd;
+#else /* CONFIG_IEEE80211AX */
+	return RNR_20_MHZ_PSD_MAX_TXPOWER;
+#endif /* CONFIG_IEEE80211AX */
+}
+
+
 static bool hostapd_eid_rnr_bss(struct hostapd_data *hapd,
 				struct hostapd_data *reporting_hapd,
 				size_t i, u8 *tbtt_count, size_t *len,
@@ -10863,7 +10912,8 @@ static bool hostapd_eid_rnr_bss(struct hostapd_data *hapd,
 	bss_param |= RNR_BSS_PARAM_CO_LOCATED;
 
 	*eid++ = bss_param;
-	*eid++ = RNR_20_MHZ_PSD_MAX_TXPOWER;
+	*eid++ = is_6ghz_op_class(op_class) ? hostapd_rnr_6ghz_psd(bss) :
+		RNR_20_MHZ_PSD_MAX_TXPOWER;
 
 #ifdef CONFIG_IEEE80211BE
 	/* Include the MLD parameters only when TBTT length is for ML RNR */
diff --git a/tests/hwsim/test_he.py b/tests/hwsim/test_he.py
index 1b1ed664d..d624de693 100644
--- a/tests/hwsim/test_he.py
+++ b/tests/hwsim/test_he.py
@@ -2138,3 +2138,52 @@ def test_he_6ghz_incumbt_sig_intf_sec40(dev, apdev):
                                           expectation)
     he_6ghz_incumbt_sig_intf_bw_reduction(dev, apdev, params, '0xC',
                                           expectation)
+
+def test_he_6ghz_rnr_psd(dev, apdev):
+    """RNR 20 MHz PSD for a co-located 6 GHz AP"""
+    check_sae_capab(dev[0])
+    hapd = None
+    hapd6 = None
+    try:
+        params = {"ssid": "rnr", "country_code": "DE", "hw_mode": "g",
+                  "channel": "1", "ieee80211ax": "1", "wpa": "2",
+                  "rsn_pairwise": "CCMP", "wpa_key_mgmt": "SAE",
+                  "sae_pwe": "1", "sae_password": "password",
+                  "ieee80211w": "2", "rnr": "1",
+                  "he_6ghz_reg_pwr_type": "0",
+                  "reg_def_cli_eirp_psd": "4", "reg_sub_cli_eirp_psd": "2"}
+        hapd = hostapd.add_ap(apdev[0], params)
+        params6 = dict(params)
+        del params6["hw_mode"]
+        params6.update({"op_class": "131", "channel": "5"})
+        hapd6 = hostapd.add_ap(apdev[1], params6, set_channel=False)
+        bssid6 = hapd6.own_addr()
+
+        dev[0].scan_for_bss(hapd.own_addr(), freq="2412", force_scan=True)
+        ie = bytes.fromhex(dev[0].get_bss(hapd.own_addr())['ie'])
+        tbtts = []
+        while len(ie) >= 2:
+            eid, data = ie[0], ie[2:2 + ie[1]]
+            ie = ie[2 + ie[1]:]
+            if eid != 201:
+                continue
+            while len(data) >= 4:
+                count = (data[0] >> 4) + 1
+                tlen = data[1]
+                info = data[4:]
+                for k in range(count):
+                    tbtt = info[k * tlen:(k + 1) * tlen]
+                    if len(tbtt) >= 13:
+                        tbtts.append((tbtt[1:7].hex(':'), tbtt[12]))
+                data = info[count * tlen:]
+        logger.info("RNR entries: " + str(tbtts))
+        # The lowest regulatory client limit: the subordinate one, 1 dBm/MHz
+        if (bssid6, 2) not in tbtts:
+            raise Exception("Unexpected RNR entries: " + str(tbtts))
+    except Exception as e:
+        if isinstance(e, Exception) and str(e) == "AP startup failed":
+            if not he_supported():
+                raise HwsimSkip("HE 6 GHz channel not supported in regulatory information")
+        raise
+    finally:
+        clear_regdom(hapd, dev)
-- 
2.47.3





More information about the Hostap mailing list