[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