@@ -97,19 +97,93 @@ def get_security_config() -> str:
9797 )
9898
9999
100+ def _channel_to_vht_centre (primary : int ) -> int | None :
101+ """Centre frequency segment for 80 MHz VHT, given the primary 20 MHz
102+ channel. Returns None if the primary isn't part of an 80 MHz block we
103+ can use in AP mode without DFS.
104+
105+ Non-DFS UNII-1 (36/40/44/48) → centre 42.
106+ UNII-3 (149/153/157/161) → centre 155.
107+ Anything else (DFS UNII-2) → caller must drop to HT40 / HT20.
108+ """
109+ if 36 <= primary <= 48 :
110+ return 42
111+ if 149 <= primary <= 161 :
112+ return 155
113+ return None
114+
115+
116+ def _ht40_secondary (primary : int ) -> str :
117+ """HT40 secondary channel position for the given 20 MHz primary.
118+ 5 GHz HT40 pairs adjacent channels (4 apart). The lower of each pair
119+ uses HT40+ (secondary above), the upper uses HT40−:
120+ 36+ / 40− 44+ / 48− 149+ / 153− 157+ / 161−
121+ Pattern: (channel // 4) odd → lower of pair → '+', even → upper → '−'."""
122+ return "+" if (primary // 4 ) % 2 == 1 else "-"
123+
124+
100125def write_hostapd_config ():
101126 security_config = get_security_config ()
127+
128+ is_5ghz = CHANNEL >= 36
129+ if is_5ghz :
130+ # 11ac VHT80 on 5 GHz when the channel sits in a non-DFS UNII block.
131+ # Falls back to HT40 if the channel doesn't fit a clean 80 MHz block
132+ # (e.g. UNII-2 DFS channels we won't touch in AP mode).
133+ ht40_dir = _ht40_secondary (CHANNEL )
134+ vht_centre = _channel_to_vht_centre (CHANNEL )
135+ # CYW43455 (Pi 5 / CM5) advertises these VHT capabilities. Including
136+ # only the bits the driver supports — extra bits cause hostapd to
137+ # silently disable VHT and drop to HT.
138+ vht_capab = (
139+ "[RXLDPC]"
140+ "[SHORT-GI-80]"
141+ "[TX-STBC-2BY1]"
142+ "[RX-STBC-1]"
143+ "[MAX-A-MPDU-LEN-EXP3]"
144+ "[MAX-MPDU-11454]"
145+ )
146+ ht_capab = (
147+ f"[HT40{ ht40_dir } ]"
148+ "[SHORT-GI-20]"
149+ "[SHORT-GI-40]"
150+ "[TX-STBC]"
151+ "[RX-STBC1]"
152+ "[DSSS_CCK-40]"
153+ )
154+ radio = (
155+ "hw_mode=a\n "
156+ f"channel={ CHANNEL } \n "
157+ "ieee80211n=1\n "
158+ "ieee80211ac=1\n "
159+ f"ht_capab={ ht_capab } \n "
160+ )
161+ if vht_centre is not None :
162+ radio += (
163+ f"vht_capab={ vht_capab } \n "
164+ "vht_oper_chwidth=1\n " # 1 = 80 MHz
165+ f"vht_oper_centr_freq_seg0_idx={ vht_centre } \n "
166+ )
167+ else :
168+ # 2.4 GHz fallback — 11n HT40 only, channels 1/6/11 don't have a
169+ # clean upper neighbour without overlap, so HT20 is safer there.
170+ radio = (
171+ "hw_mode=g\n "
172+ f"channel={ CHANNEL } \n "
173+ "ieee80211n=1\n "
174+ "ht_capab=[SHORT-GI-20]\n "
175+ )
176+
102177 config = f"""
103178interface={ WIFI_IFACE }
104179driver=nl80211
105180ssid={ SSID }
106- hw_mode=a
107- channel={ CHANNEL }
108181country_code={ COUNTRY_CODE }
109- ieee80211n =1
110- ieee80211ac=1
111- ignore_broadcast_ssid=0
182+ ieee80211d =1
183+ ieee80211h=0
184+ { radio } ignore_broadcast_ssid=0
112185wmm_enabled=1
186+ beacon_int=100
113187{ security_config }
114188wpa_passphrase={ PASSPHRASE }
115189"""
0 commit comments