[RFC 8/8] tests: add S1G channel tests

Lachlan Hodges lachlan.hodges at morsemicro.com
Wed May 27 23:38:57 PDT 2026


Add a small suite of S1G tests which test various channel permutations,
the NO_PRIMARY flag and correct AID Response element configuring and
parsing.

Signed-off-by: Lachlan Hodges <lachlan.hodges at morsemicro.com>
---
 tests/hwsim/example-hostapd.config        |   1 +
 tests/hwsim/example-wpa_supplicant.config |   1 +
 tests/hwsim/test_s1g.py                   | 245 ++++++++++++++++++++++
 3 files changed, 247 insertions(+)
 create mode 100644 tests/hwsim/test_s1g.py

diff --git a/tests/hwsim/example-hostapd.config b/tests/hwsim/example-hostapd.config
index 41611b0f2f16..6a01cd61c38d 100644
--- a/tests/hwsim/example-hostapd.config
+++ b/tests/hwsim/example-hostapd.config
@@ -128,3 +128,4 @@ CONFIG_PROCESS_COORDINATION=y
 CONFIG_ENC_ASSOC=y
 CONFIG_PMKSA_PRIVACY=y
 CONFIG_IEEE8021X_AUTH=y
+CONFIG_IEEE80211AH=y
diff --git a/tests/hwsim/example-wpa_supplicant.config b/tests/hwsim/example-wpa_supplicant.config
index c5b364757afa..48283fc4dd8f 100644
--- a/tests/hwsim/example-wpa_supplicant.config
+++ b/tests/hwsim/example-wpa_supplicant.config
@@ -176,3 +176,4 @@ CONFIG_NAN=y
 CONFIG_ENC_ASSOC=y
 CONFIG_PMKSA_PRIVACY=y
 CONFIG_IEEE8021X_AUTH=y
+CONFIG_IEEE80211AH=y
diff --git a/tests/hwsim/test_s1g.py b/tests/hwsim/test_s1g.py
new file mode 100644
index 000000000000..39ebe6efe871
--- /dev/null
+++ b/tests/hwsim/test_s1g.py
@@ -0,0 +1,245 @@
+# Test cases for IEEE 802.11ah S1G operation
+# Copyright (c) 2026 Morse Micro
+#
+# This software may be distributed under the terms of the BSD license.
+# See README for more details.
+
+import logging
+logger = logging.getLogger()
+import hostapd
+import hwsim_utils
+from utils import *
+
+# 4 MHz op 2 MHz primary lower 1 MHz
+def test_s1g_4mhz_op_2mhz_pri_lower_loc(dev, apdev):
+    """S1G 4 MHz op with 2 MHz primary lower location"""
+    check_sae_capab(dev[0])
+    hapd = None
+    clear_scan_cache(apdev[0])
+    try:
+        params = hostapd.wpa3_params(ssid="s1g-1mhz", password="s1gpass")
+        params.update({
+            "country_code": "US",
+            "ieee80211ah": "1",
+            "channel": "41",
+            "op_class": "70",
+            "hw_mode": "ah",
+            "s1g_oper_centr_freq_idx": "40",
+            "s1g_primary_2mhz": "1",
+        })
+        hapd = hostapd.add_ap(apdev[0], params)
+        status = hapd.get_status()
+        if status.get("ieee80211ah") != "1":
+            raise Exception("AP did not report IEEE 802.11ah enabled")
+        dev[0].connect("s1g-1mhz", sae_password="s1gpass", key_mgmt="SAE",
+                       scan_freq="922", scan_freq_offset="500", sae_pwe="1",
+                       ieee80211w="2")
+    except Exception as e:
+        if isinstance(e, Exception) and str(e) == "AP startup failed":
+            raise HwsimSkip("S1G operation not supported in this environment")
+        raise
+    finally:
+        dev[0].request("DISCONNECT")
+        dev[0].wait_disconnected()
+        clear_regdom(hapd, dev)
+
+# 2 MHz op 2 MHz primary upper 1 MHz
+def test_s1g_2mhz_op_2mhz_pri_upper_loc(dev, apdev):
+    """S1G 2 MHz op with 2 MHz primary upper location"""
+    check_sae_capab(dev[0])
+    hapd = None
+    clear_scan_cache(apdev[0])
+    try:
+        params = hostapd.wpa3_params(ssid="s1g-1mhz", password="s1gpass")
+        params.update({
+            "country_code": "US",
+            "ieee80211ah": "1",
+            "channel": "31",
+            "op_class": "69",
+            "hw_mode": "ah",
+            "s1g_oper_centr_freq_idx": "30",
+            "s1g_primary_2mhz": "1",
+        })
+        hapd = hostapd.add_ap(apdev[0], params)
+        status = hapd.get_status()
+        if status.get("ieee80211ah") != "1":
+            raise Exception("AP did not report IEEE 802.11ah enabled")
+        dev[0].connect("s1g-1mhz", sae_password="s1gpass", key_mgmt="SAE",
+                       scan_freq="917", scan_freq_offset="500", sae_pwe="1",
+                       ieee80211w="2")
+    except Exception as e:
+        if isinstance(e, Exception) and str(e) == "AP startup failed":
+            raise HwsimSkip("S1G operation not supported in this environment")
+        raise
+    finally:
+        dev[0].request("DISCONNECT")
+        dev[0].wait_disconnected()
+        clear_regdom(hapd, dev)
+
+# 2 MHz op 2 MHz primary lower 1 MHz
+def test_s1g_2mhz_op_2mhz_pri_lower_loc(dev, apdev):
+    """S1G 2 MHz op with 2 MHz primary lower location"""
+    check_sae_capab(dev[0])
+    hapd = None
+    clear_scan_cache(apdev[0])
+    try:
+        params = hostapd.wpa3_params(ssid="s1g-1mhz", password="s1gpass")
+        params.update({
+            "country_code": "US",
+            "ieee80211ah": "1",
+            "channel": "29",
+            "op_class": "69",
+            "hw_mode": "ah",
+            "s1g_oper_centr_freq_idx": "30",
+            "s1g_primary_2mhz": "1",
+        })
+        hapd = hostapd.add_ap(apdev[0], params)
+        status = hapd.get_status()
+        if status.get("ieee80211ah") != "1":
+            raise Exception("AP did not report IEEE 802.11ah enabled")
+        dev[0].connect("s1g-1mhz", sae_password="s1gpass", key_mgmt="SAE",
+                       scan_freq="916", scan_freq_offset="500", sae_pwe="1",
+                       ieee80211w="2")
+    except Exception as e:
+        if isinstance(e, Exception) and str(e) == "AP startup failed":
+            raise HwsimSkip("S1G operation not supported in this environment")
+        raise
+    finally:
+        dev[0].request("DISCONNECT")
+        dev[0].wait_disconnected()
+        clear_regdom(hapd, dev)
+
+# 2 MHz 1 MHz primary lower
+def test_s1g_2mhz_op_1mhz_pri(dev, apdev):
+    """S1G 2 MHz op with 1 MHz primary"""
+    check_sae_capab(dev[0])
+    hapd = None
+    clear_scan_cache(apdev[0])
+    try:
+        params = hostapd.wpa3_params(ssid="s1g-1mhz", password="s1gpass")
+        params.update({
+            "country_code": "US",
+            "ieee80211ah": "1",
+            "channel": "5",
+            "op_class": "69",
+            "hw_mode": "ah",
+            "s1g_oper_centr_freq_idx": "6",
+        })
+        hapd = hostapd.add_ap(apdev[0], params)
+        status = hapd.get_status()
+        if status.get("ieee80211ah") != "1":
+            raise Exception("AP did not report IEEE 802.11ah enabled")
+        dev[0].connect("s1g-1mhz", sae_password="s1gpass", key_mgmt="SAE",
+                       scan_freq="904", scan_freq_offset="500", sae_pwe="1",
+                       ieee80211w="2")
+    except Exception as e:
+        if isinstance(e, Exception) and str(e) == "AP startup failed":
+            raise HwsimSkip("S1G operation not supported in this environment")
+        raise
+    finally:
+        dev[0].request("DISCONNECT")
+        dev[0].wait_disconnected()
+        clear_regdom(hapd, dev)
+
+def test_s1g_1mhz(dev, apdev):
+    """S1G 1 MHz AP with SAE"""
+    check_sae_capab(dev[0])
+    hapd = None
+    clear_scan_cache(apdev[0])
+    try:
+        params = hostapd.wpa3_params(ssid="s1g-1mhz", password="s1gpass")
+        params.update({
+            "country_code": "US",
+            "ieee80211ah": "1",
+            "channel": "3",
+            "op_class": "68",
+            "hw_mode": "ah",
+            "s1g_oper_centr_freq_idx": "0",
+        })
+        hapd = hostapd.add_ap(apdev[0], params)
+        status = hapd.get_status()
+        if status.get("ieee80211ah") != "1":
+            raise Exception("AP did not report IEEE 802.11ah enabled")
+        dev[0].connect("s1g-1mhz", sae_password="s1gpass", key_mgmt="SAE",
+                       scan_freq="903", scan_freq_offset="500", sae_pwe="1",
+                       ieee80211w="2")
+    except Exception as e:
+        if isinstance(e, Exception) and str(e) == "AP startup failed":
+            raise HwsimSkip("S1G operation not supported in this environment")
+        raise
+    finally:
+        dev[0].request("DISCONNECT")
+        dev[0].wait_disconnected()
+        clear_regdom(hapd, dev)
+
+def test_s1g_1mhz_no_primary(dev, apdev):
+    """S1G 1 MHz AP using 1 MHz primary that is marked as NO_PRIMARY"""
+    check_sae_capab(dev[0])
+    hapd = None
+    clear_scan_cache(apdev[0])
+    try:
+        params = hostapd.wpa3_params(ssid="s1g-1mhz", password="s1gpass")
+        params.update({
+            "country_code": "US",
+            "ieee80211ah": "1",
+            "channel": "1",
+            "op_class": "68",
+            "hw_mode": "ah",
+            "s1g_oper_centr_freq_idx": "0",
+        })
+        hapd = hostapd.add_ap(apdev[0], params, wait_enabled=False)
+        ev = hapd.wait_event(["AP-DISABLED", "AP-ENABLED"], timeout=10)
+        if not ev:
+            raise Exception("AP setup failure timed out")
+        if "AP-ENABLED" in ev:
+            raise Exception("S1G 1 MHz NO_PRIMARY channel accepted")
+    except Exception as e:
+        if isinstance(e, Exception) and str(e) == "AP startup failed":
+            raise HwsimSkip("S1G operation not supported in this environment")
+        raise
+    finally:
+        clear_regdom(hapd, dev)
+
+def test_s1g_multi_sta_aid(dev, apdev):
+    """S1G AID response is correctly built and parsed"""
+    for i in range(3):
+        check_sae_capab(dev[i])
+    hapd = None
+    clear_scan_cache(apdev[0])
+    try:
+        params = hostapd.wpa3_params(ssid="s1g-aid", password="s1gpass")
+        params.update({
+            "country_code": "US",
+            "ieee80211ah": "1",
+            "channel": "5",
+            "op_class": "69",
+            "hw_mode": "ah",
+            "s1g_oper_centr_freq_idx": "6",
+        })
+        hapd = hostapd.add_ap(apdev[0], params)
+        status = hapd.get_status()
+        if status.get("ieee80211ah") != "1":
+            raise Exception("AP did not report IEEE 802.11ah enabled")
+        for i in range(3):
+            dev[i].connect("s1g-aid", sae_password="s1gpass", key_mgmt="SAE",
+                           scan_freq="904", scan_freq_offset="500",
+                           sae_pwe="1", ieee80211w="2")
+        for i in range(3):
+            hapd.wait_sta()
+        aid = []
+        for i in range(3):
+            aid.append(int(hapd.get_sta(dev[i].own_addr())['aid']))
+        logger.info("S1G assigned AIDs: " + str(aid))
+        if len(set(aid)) != 3:
+            raise Exception("AP did not assign unique AID to each S1G STA")
+        if any(a == 0 for a in aid):
+            raise Exception("AP assigned AID 0 to an S1G STA")
+    except Exception as e:
+        if isinstance(e, Exception) and str(e) == "AP startup failed":
+            raise HwsimSkip("S1G operation not supported in this environment")
+        raise
+    finally:
+        for i in range(3):
+            dev[i].request("DISCONNECT")
+            dev[i].wait_disconnected()
+        clear_regdom(hapd, dev)
-- 
2.43.0




More information about the Hostap mailing list