From 6e5d31698737eee101b499e99035d2534297a497 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:37:36 +0300 Subject: [PATCH 1/2] jaguar1: HT40 centre channel follows the offset (vendor rtw_get_center_ch) The 40 MHz centre came from a hand-written channel table that ignored the HT40 offset. On 2.4 GHz it only listed primaries 4 and 8, so any other primary stayed uncentred: ch6 HT40+ tuned RF 0x18 to channel 6 instead of 8, 10 MHz low, and every HT40 frame was lost while 20 MHz worked. The table was also wrong on UNII-3 (153->155, 161->163) and for the non-default offset of the pairs it did list. Use the vendor rule instead (UPPER -> primary - 2, otherwise + 2, as Kestrel already does) and drop the table. 8812AU, ch6 HT40+: RF 0x18 0x07406 -> 0x07408 (vendor driver: 0x07408); delivery 0 % -> 36-54 % (vendor driver on the same unit: 52-61 %). ch36/ch40 HT40 unchanged at 94-97 %. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/jaguar1/RadioManagementModule.cpp | 41 ++++----------------------- 1 file changed, 6 insertions(+), 35 deletions(-) diff --git a/src/jaguar1/RadioManagementModule.cpp b/src/jaguar1/RadioManagementModule.cpp index 21b4c022..b945deda 100644 --- a/src/jaguar1/RadioManagementModule.cpp +++ b/src/jaguar1/RadioManagementModule.cpp @@ -12,42 +12,8 @@ extern "C" { #include #include #include -#include #include -namespace { - -int get_40mhz_center_channel(int channel) { - static const std::unordered_map channel_map = { - // 2.4GHz 40MHz configuration: only one valid center channel (6) - {4, 6}, // primary below center (extension above) - {8, 6}, // primary above center (extension below) - - // 5GHz UNII-1 - {36, 38}, {40, 38}, - // 5GHz UNII-1 / UNII-2 - {44, 46}, {48, 46}, - // 5GHz UNII-2A - {52, 54}, {56, 54}, {60, 62}, {64, 62}, - // 5GHz UNII-2C - {100, 102}, {104, 102}, - {108, 110}, {112, 110}, - {116, 118}, {120, 118}, - {124, 126}, {128, 126}, - // 5GHz UNII-2E (if applicable) - {132, 134}, {136, 134}, - {140, 142}, {144, 142}, - // 5GHz UNII-3 - {149, 151}, {153, 155}, - {157, 159}, {161, 163} - }; - - auto it = channel_map.find(channel); - return (it != channel_map.end()) ? it->second : channel; -} - -} - RadioManagementModule::RadioManagementModule( RtlAdapter device, std::shared_ptr eepromManager, Logger_t logger, const devourer::DeviceConfig &cfg) @@ -240,7 +206,12 @@ static uint8_t rtw_get_center_ch(uint8_t channel, ChannelWidth_t chnl_bw, else if (channel <= 14) center_ch = 7; } else if (chnl_bw == ChannelWidth_t::CHANNEL_WIDTH_40) { - center_ch = get_40mhz_center_channel(center_ch); + /* The offset decides the centre, not a channel-plan table: on 2.4 GHz any + * primary can carry an HT40 pair, and a centre left on the primary tunes + * the RF 10 MHz low, so every HT40 frame is lost. UPPER = primary is the + * upper 20 (centre below); LOWER and DONT_CARE take HT40+. */ + center_ch = chnl_offset == HAL_PRIME_CHNL_OFFSET_UPPER ? channel - 2 + : channel + 2; } else if (chnl_bw == ChannelWidth_t::CHANNEL_WIDTH_20 || chnl_bw == ChannelWidth_t::CHANNEL_WIDTH_5 || chnl_bw == ChannelWidth_t::CHANNEL_WIDTH_10) { From b5ffaf45db20c04a5d43cc9e3412ce98f8eb83c0 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:06:49 +0300 Subject: [PATCH 2/2] jaguar1: fit a 2.4 GHz HT40 request that leaves the channel grid Offset arithmetic alone can leave the band: ch13 HT40+ gives centre 15, which Jaguar1 classifies as 5 GHz, and ch1 HT40- wraps to 255. The vendor never reaches rtw_get_center_ch with such a pair because its channel plan rejects it first. On 2.4 GHz a pair must stay inside 1-13 (HT40+ needs a primary of 1-9, HT40- one of 5-13), so a request that does not fit takes the side that does, with a warning. 5 GHz is left to the caller. The caller's requested offset is kept apart from the fitted one, so a FastRetune hop back from the grid edge restores the requested side instead of inheriting the forced one. 8812AU TX, two 8822B receivers: ch13 requested HT40+ -> RF 0x18 0x0740B (centre 11), 91 / 89 %; ch1 requested HT40- -> 0x07403 (centre 3), 91 / 87 %. Hop 6,13,6 at HT40+: 0x07408, 0x0740B, 0x07408 on both the fast and full paths. ch6 and ch36 unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/jaguar1/RadioManagementModule.cpp | 35 ++++++++++++++++++++++++++- src/jaguar1/RadioManagementModule.h | 4 +++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/jaguar1/RadioManagementModule.cpp b/src/jaguar1/RadioManagementModule.cpp index b945deda..37d8b9dc 100644 --- a/src/jaguar1/RadioManagementModule.cpp +++ b/src/jaguar1/RadioManagementModule.cpp @@ -179,6 +179,22 @@ void RadioManagementModule::hw_var_set_monitor() { _device.rtw_write16(REG_RXFLTMAP2, value_rxfltmap2); } +/* A 2.4 GHz HT40 pair must stay inside channels 1-13: HT40+ needs a primary + * of 1-9, HT40- one of 5-13. Off the grid the centre arithmetic leaves the + * band (ch13 HT40+ -> 15, classified as 5 GHz; ch1 HT40- wraps to 255), so + * such a request takes the side that fits. The vendor never gets here with + * one: its channel plan rejects the pair first. 5 GHz is left to the caller, + * which owns the channel plan there. */ +static uint8_t fit_2g4_ht40_offset(uint8_t channel, uint8_t chnl_offset) { + if (channel > 14) + return chnl_offset; + if (channel < 5) + return HAL_PRIME_CHNL_OFFSET_LOWER; + if (channel > 9) + return HAL_PRIME_CHNL_OFFSET_UPPER; + return chnl_offset; +} + static uint8_t rtw_get_center_ch(uint8_t channel, ChannelWidth_t chnl_bw, uint8_t chnl_offset) { uint8_t center_ch = channel; @@ -233,6 +249,17 @@ void RadioManagementModule::set_channel_bwmode(uint8_t channel, _logger->info("[{}] ch = {}, offset = {}, bwmode = {}", __func__, unsigned(channel), unsigned(channel_offset), (int)bwmode); + if (bwmode == ChannelWidth_t::CHANNEL_WIDTH_40) { + _req40MhzPrimeSc = channel_offset; + const uint8_t fitted = fit_2g4_ht40_offset(channel, channel_offset); + if (fitted != channel_offset) { + _logger->warn("ch {} HT40 offset {} leaves the 2.4 GHz grid — using {}", + unsigned(channel), unsigned(channel_offset), + unsigned(fitted)); + channel_offset = fitted; + } + } + /* The full path may rewrite RF_CHNLBW (0x18) bandwidth bits + fc_area + * spur regs, so the fast_retune caches are stale after this. */ _rf_chnlbw_cached = false; @@ -285,6 +312,10 @@ bool RadioManagementModule::fast_retune(uint8_t channel, bool cache_rf) { * tests/hop_parity_check.sh. */ if (cache_rf && _eepromManager->version_id.ICType != CHIP_8814A) { InitTimer timer(_logger, "fast_retune"); + /* A hop to the grid edge can force the other HT40 side; the SubChnl + * rewrite below follows the changed offset. */ + if (_currentChannelBw == ChannelWidth_t::CHANNEL_WIDTH_40) + _cur40MhzPrimeSc = fit_2g4_ht40_offset(channel, _req40MhzPrimeSc); const uint8_t center = rtw_get_center_ch(channel, _currentChannelBw, _cur40MhzPrimeSc); _currentChannel = center; @@ -315,8 +346,10 @@ bool RadioManagementModule::fast_retune(uint8_t channel, bool cache_rf) { * channel+BW parity) but skip the per-rate TX-power loop + pwrtrk tick via * _fast_skip_heavy. */ InitTimer timer(_logger, "fast_retune"); - const uint8_t offset = _cur40MhzPrimeSc; const ChannelWidth_t bw = _currentChannelBw; + const uint8_t offset = bw == ChannelWidth_t::CHANNEL_WIDTH_40 + ? _req40MhzPrimeSc + : _cur40MhzPrimeSc; _fast_skip_heavy = true; set_channel_bwmode(channel, offset, bw); _fast_skip_heavy = false; diff --git a/src/jaguar1/RadioManagementModule.h b/src/jaguar1/RadioManagementModule.h index aa71583c..7ac32b30 100644 --- a/src/jaguar1/RadioManagementModule.h +++ b/src/jaguar1/RadioManagementModule.h @@ -110,6 +110,10 @@ class RadioManagementModule { uint32_t _bw_cached_8ac = 0; bool _bw_cache_valid = false; uint8_t _cur40MhzPrimeSc; + /* The HT40 offset the caller asked for, before fit_2g4_ht40_offset: a hop + * to the 2.4 GHz grid edge may force the other side, and the next hop must + * fit from the request again, not inherit the forced side. */ + uint8_t _req40MhzPrimeSc = HAL_PRIME_CHNL_OFFSET_DONT_CARE; uint8_t _cur80MhzPrimeSc; uint8_t _currentCenterFrequencyIndex; uint8_t power = 16;