From c29f45867c1509a88a986a52c1a0bdd45a28e515 Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 7 Dec 2025 15:17:07 +0100 Subject: [PATCH] Add dynamic center frequency + HT40 direction for 802.11ax/be --- .../Networking/Hotspot/HostapdManager.php | 254 +++++++++++++++++- 1 file changed, 249 insertions(+), 5 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/HostapdManager.php b/src/RaspAP/Networking/Hotspot/HostapdManager.php index 0bcbbcb4..495722b9 100644 --- a/src/RaspAP/Networking/Hotspot/HostapdManager.php +++ b/src/RaspAP/Networking/Hotspot/HostapdManager.php @@ -191,8 +191,6 @@ class HostapdManager $config[] = 'ssid=' . $params['ssid']; $config[] = 'channel=' . $params['channel']; - // choose VHT segment index (fallback only if required) - $vht_freq_idx = ($params['channel'] < RASPI_5GHZ_CHANNEL_MIN) ? 42 : 155; $hwMode = isset($params['hw_mode']) ? $params['hw_mode'] : ''; // validate channel width for 802.11ax/be @@ -211,14 +209,24 @@ class HostapdManager $modeSettings = getDefaultNetOpts('hostapd', 'modes', $hwMode); $settings = $modeSettings[$hwMode]['settings'] ?? []; - error_log("HostapdManager::buildConfig() -> settings\n" . var_export($settings, true)); + // extract channel width from settings to calculate center frequency + $chwidth = $this->extractChannelWidth($settings, $hwMode); + + // calculate center frequency indices based on channel + width + $vht_freq_idx = $this->calculateCenterFreqIndex((int)$params['channel'], $chwidth); + $he_freq_idx = $vht_freq_idx; // For most cases, HE and VHT use the same center frequency + + // calculate HT40 direction based on channel and width + $ht40_dir = $this->calculateHT40Direction((int)$params['channel'], $chwidth); if (!empty($settings)) { foreach ($settings as $line) { if (!is_string($line)) { continue; } - $replaced = str_replace('{VHT_FREQ_IDX}', (string) $vht_freq_idx ?? '',$line); + $replaced = str_replace('{VHT_FREQ_IDX}', (string) $vht_freq_idx ?? '', $line); + $replaced = str_replace('{HE_FREQ_IDX}', (string) $he_freq_idx ?? '', $replaced); + $replaced = str_replace('{HT40_DIR}', (string) $ht40_dir ?? '', $replaced); $config[] = $replaced; } } @@ -498,5 +506,241 @@ class HostapdManager return $capabilities[$mode] ?? $capabilities['g']; } -} + /** + * Extracts channel width from mode settings + * + * @param array $settings mode settings array + * @param string $hwMode hardware mode (ac, ax, be) + * @return int channel width in MHz (20, 40, 80, 160, 320) + */ + private function extractChannelWidth(array $settings, string $hwMode): int + { + $chwidthParam = ''; + // determine parameter based on mode + if ($hwMode === 'ac') { + $chwidthParam = 'vht_oper_chwidth'; + } elseif ($hwMode === 'ax') { + $chwidthParam = 'he_oper_chwidth'; + } elseif ($hwMode === 'be') { + $chwidthParam = 'eht_oper_chwidth'; + } else { + return 20; // 20 MHz default for other modes + } + + // parse settings to find channel width + foreach ($settings as $line) { + if (!is_string($line)) { + continue; + } + + // skip comments + if (strpos(trim($line), '#') === 0) { + continue; + } + + if (strpos($line, $chwidthParam . '=') !== false) { + $parts = explode('=', $line, 2); + if (count($parts) === 2) { + // extract numeric value + $value = trim($parts[1]); + // remove any inline comments + $value = preg_replace('/\s*#.*$/', '', $value); + $chwidthCode = (int) $value; + + // convert hostapd encoding to MHz based on mode + if ($hwMode === 'be') { + // EHT uses: 0=20, 1=40, 2=80, 3=160, 4=320 + switch ($chwidthCode) { + case 0: return 20; + case 1: return 40; + case 2: return 80; + case 3: return 160; + case 4: return 320; + default: return 20; + } + } else { + // VHT/HE uses: 0=20/40, 1=80, 2=160, 3=80+80 + switch ($chwidthCode) { + case 0: return 40; + case 1: return 80; + case 2: return 160; + case 3: return 160; // 80+80 treated as 160 + default: return 20; + } + } + } + } + } + + return 20; // default to 20 MHz channel if not found + } + + /** + * Calculates center frequency segment 0 index for given channel and width + * + * @param int $channel primary channel number + * @param int $chwidthMHz channel width in MHz (20, 40, 80, 160, 320) + * @return int center frequency segment 0 index + */ + private function calculateCenterFreqIndex(int $channel, int $chwidthMHz): int + { + // determine band based on channel number + $is24GHz = ($channel >= 1 && $channel <= 14); + $is5GHz = ($channel >= 36 && $channel <= 177); + $is6GHz = ($channel >= 1 && $channel <= 233 && !$is24GHz); // 6 GHz uses 1-233 + + // 20 MHz - center is always primary channel + if ($chwidthMHz <= 20) { + return $channel; + } + + // 2.4 GHz band + if ($is24GHz) { + if ($chwidthMHz == 40) { + // for 2.4 GHz, typically use HT40+ (center is primary + 2) + // channels 1-7 use HT40+, channels 8-13 use HT40- + return ($channel <= 7) ? $channel + 2 : $channel - 2; + } + // wider bandwidths not supported on 2.4 GHz + return $channel; + } + + // 5 GHz band + if ($is5GHz) { + if ($chwidthMHz == 40) { + // HT40+ configuration: center = primary + 2 + // adjust for upper/lower position in 40 MHz pair + if (in_array($channel, [36, 44, 52, 60, 100, 108, 116, 124, 132, 140, 149, 157, 165, 173])) { + return $channel + 2; + } else { + return $channel - 2; + } + } + + if ($chwidthMHz == 80) { + // map channel to 80 MHz center frequency + if ($channel >= 36 && $channel <= 48) return 42; + if ($channel >= 52 && $channel <= 64) return 58; + if ($channel >= 100 && $channel <= 112) return 106; + if ($channel >= 116 && $channel <= 128) return 122; + if ($channel >= 132 && $channel <= 144) return 138; + if ($channel >= 149 && $channel <= 161) return 155; + if ($channel >= 165 && $channel <= 177) return 171; + } + + if ($chwidthMHz == 160) { + // map channel to 160 MHz center frequency + if ($channel >= 36 && $channel <= 64) return 50; + if ($channel >= 100 && $channel <= 128) return 114; + // channels 149-177 don't support 160 MHz in most regions + if ($channel >= 149 && $channel <= 177) return 163; + } + } + + // 6 GHz band (UNII-5 through UNII-8) + if ($is6GHz && !$is24GHz) { + // 6 GHz uses different channel numbering: 1, 5, 9, 13, ... (every 4) + if ($chwidthMHz == 40) { + // center is at the midpoint between two 20 MHz channels + return $channel + 2; + } + + if ($chwidthMHz == 80) { + // calculate 80 MHz center + $blockStart = (int)(($channel - 1) / 16) * 16 + 1; + return $blockStart + 6; + } + + if ($chwidthMHz == 160) { + // calculate 160 MHz center + $blockStart = (int)(($channel - 1) / 32) * 32 + 1; + return $blockStart + 14; + } + + if ($chwidthMHz == 320) { + // calculate 320 MHz center + $blockStart = (int)(($channel - 1) / 64) * 64 + 1; + return $blockStart + 30; + } + } + + // fallback: return primary channel + return $channel; + } + + /** + * Calculates HT40 direction (+ or -) based on channel and bandwidth + * + * @param int $channel primary channel number + * @param int $chwidthMHz channel width in MHz + * @return string HT40 direction: "HT40+" or "HT40-" or "" for 20MHz + */ + private function calculateHT40Direction(int $channel, int $chwidthMHz): string + { + // only applicable for 40 MHz and wider on 5 GHz + if ($chwidthMHz < 40) { + return ''; + } + + $is24GHz = ($channel >= 1 && $channel <= 14); + $is5GHz = ($channel >= 36 && $channel <= 177); + + // 2.4 GHz band + if ($is24GHz) { + // channels 1-7 use HT40+, channels 8-13 use HT40- + return ($channel <= 7) ? 'HT40+' : 'HT40-'; + } + + // 5 GHz band + if ($is5GHz) { + if ($chwidthMHz == 40) { + // for pure 40 MHz mode + if (in_array($channel, [36, 44, 52, 60, 100, 108, 116, 124, 132, 140, 149, 157, 165, 173])) { + return 'HT40+'; + } else { + return 'HT40-'; + } + } + + if ($chwidthMHz >= 80) { + // for 80 MHz and wider, determine based on position within the 80 MHz block + // lower half of 80 MHz block uses HT40+, upper half uses HT40- + + // determine which 80 MHz block this channel belongs to + if ($channel >= 36 && $channel <= 48) { + // block: 36, 40, 44, 48 (center 42) + return ($channel <= 40) ? 'HT40+' : 'HT40-'; + } + if ($channel >= 52 && $channel <= 64) { + // block: 52, 56, 60, 64 (center 58) + return ($channel <= 56) ? 'HT40+' : 'HT40-'; + } + if ($channel >= 100 && $channel <= 112) { + // block: 100, 104, 108, 112 (center 106) + return ($channel <= 104) ? 'HT40+' : 'HT40-'; + } + if ($channel >= 116 && $channel <= 128) { + // block: 116, 120, 124, 128 (center 122) + return ($channel <= 120) ? 'HT40+' : 'HT40-'; + } + if ($channel >= 132 && $channel <= 144) { + // block: 132, 136, 140, 144 (center 138) + return ($channel <= 136) ? 'HT40+' : 'HT40-'; + } + if ($channel >= 149 && $channel <= 161) { + // block: 149, 153, 157, 161 (center 155) + return ($channel <= 153) ? 'HT40+' : 'HT40-'; + } + if ($channel >= 165 && $channel <= 177) { + // block: 165, 169, 173, 177 (center 171) + return ($channel <= 169) ? 'HT40+' : 'HT40-'; + } + } + } + + // default fallback + return 'HT40+'; + } + +}