Skip to content

Band downsteer - #24

Open
rdmitry0911 wants to merge 6 commits into
openwrt:masterfrom
rdmitry0911:band-downsteer
Open

rdmitry0911 wants to merge 6 commits into
openwrt:masterfrom
rdmitry0911:band-downsteer

Conversation

@rdmitry0911

Copy link
Copy Markdown

No description provided.

rdmitry0911 and others added 6 commits September 5, 2026 11:00
usteer's band steering only moves clients UP (2.4 -> 5/6 GHz). A client that
camps on a weak 6 GHz signal is never moved down, even when the same AP's
5 GHz would be far stronger, because roaming only steers to candidates whose
per-STA signal usteer has measured, and a client glued to 6 GHz is not heard
on the lower bands.

Add an opt-in fallback: when the roam state machine reaches SCAN_DONE with no
measured better candidate and the client sits on an upper band (freq > 4000)
below the new band_downsteer_snr threshold, send a directed BSS-TM request to
the next-lower band of the SAME AP (6->5, then 5->2.4). Measured roaming to
another AP keeps priority; down-steering only runs when usteer would otherwise
give up. min_snr / min_connect_snr on the upper band prevent an immediate
bounce back.

Also wire the new option through the OpenWrt init script and document it in
the sample config and README.

Verified on a 4x Airoha AN7581 (MT7996) mesh with an iPhone that otherwise
clung to 6 GHz at -85 dBm: the client is moved to the same AP's 5 GHz and
streams normally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FT9j3wHzwrFbMaxRdn5x3T
On a fluctuating signal a down-steered client could bounce straight back up to
the weak band as soon as its signal briefly peaked above the threshold. Add a
time-based hold: for band_downsteer_hold ms after a down-steer, refuse
probe/assoc for any band higher than the one the client was moved to. This is
hysteresis independent of the instantaneous signal, so brief peaks no longer
pull the client back up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FT9j3wHzwrFbMaxRdn5x3T
Refuse association to an upper band (>2.4 GHz) on ANY node when the client
signal is below assoc_min_snr; 2.4 GHz is exempt so a client that is weak on
all upper bands still has a landing band and cannot be locked out. Enforced
independently of assoc_steering. Admission-only: never kicks a connected
client, so unlike min_snr it cannot cause an assoc/kick storm.

Closes the cross-AP gap that per-AP band_downsteer_hold cannot: the hold stops
bounce-back to the SAME AP upper band, assoc_min_snr stops a jump to another
node upper band that is equally weak.
After a down-steer BSS-TM, if band_downsteer_disassoc (ms) is set and the client
is still on the upper band after that delay, kick it. Handles clients (notably
iOS) that ignore the gentle BSS-TM and cling to a dying upper band. Safe with
assoc_min_snr: the kicked client cannot reassociate to the weak upper band, so
it lands on 2.4/5 GHz and cannot bounce back -- no kick storm (unlike min_snr).
0 = gentle (suggestion only, previous behaviour).
When a client rejects a BSS-TM (status_code != 0), keep the already-scheduled
fallback kick instead of cancelling it. Cancelling left stubborn clients
(notably iPhones) stuck for minutes on soft-steering retries and defeated the
forced band-downsteer, which relies on kick_time. Ask nicely first, deauth if
the client refuses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWdWhnrgq2Mq8nuuMmtsNL
The band_steering_signal_threshold field existed in the config struct,
was referenced by band_steering.c, listed in the init.d option loop and
documented in config/usteer -- but it was MISSING from the ubus.c _cfg
config table. So set_config silently dropped it: uci could never change
it and the daemon always used the hardcoded main.c default of 5.

With threshold=5 the up-steer (band_steering) only fires when a client's
signal has risen >5 dB above its tracked minimum (i.e. it is physically
approaching the AP); a client that connected already-strong on 2.4 GHz
and stays put is never steered up. Exposing the knob lets it be set to 0,
which disables that "signal must be improving" gate and up-steers any
BSS-TM-capable client whose 2.4 GHz signal is >= band_steering_min_snr.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWdWhnrgq2Mq8nuuMmtsNL
@NilsRo

NilsRo commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Band steering has to be done as the station does not send out measurement reports if the SNR is too good as common for 2,4Ghz. This should not be the case here with -85 dBm. All frequencies of the local AP are used as targets so the standard rules should be applied here.

You should check what are the answers of the station and if measurement reports are handled correctly.

@rdmitry0911

Copy link
Copy Markdown
Author

Thanks — good point, let me clarify, because it turned out not to be about the 6 GHz SNR being "too good".

The trigger is the client's 802.11k capability. On the affected clients (iPhone, and iOS in general) hostapd get_clients reports rrm = 0x42Neighbor Report + Beacon Table measurement only, with neither active (bit 5) nor passive (bit 4) beacon measurement, and mbo:false. So the station only ever returns beacons already in its table. While it is camped on 6 GHz it does not scan the lower bands, so its beacon table holds no fresh 5/2.4 GHz entries and a beacon-measurement request comes back with no lower-band result — even at -85 dBm. usteer therefore has no measured per-STA candidate on the lower bands and the roam state machine reaches SCAN_DONE with nothing to steer to, leaving the client on the dying upper band.

"All local-AP frequencies are targets" is true, but a target still needs a measured per-STA signal to be selected, and that measurement never arrives for the lower band because the client isn't heard there. band_downsteer is an opt-in fallback strictly for that SCAN_DONE-with-no-candidate case: it sends a directed BSS-TM to the next-lower band of the same AP, which is physically guaranteed reachable without needing a measurement. Measured cross-AP roaming keeps priority; this only runs when usteer would otherwise give up.

AP-side RRM is fully enabled here (rrm_neighbor_report=1, rrm_beacon_report=1, populated rrm_nr), so this isn't a report-handling bug on usteer's side — it's the client's capability. iOS also routinely ignores the gentle BSS-TM, which is why the forced-disassoc knob is optional on top.

Happy to share the get_clients rrm dump and usteer debug logs (the SCAN_DONE / "no better candidate" path) if useful.

@NilsRo

NilsRo commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Your comments looks KI generated...but anyway...that's not a band steering issue but the request for measurement send to the stations seems not optimal. That looks like the main reason for this behaviour. But have to check this further as I do not own a 6Ghz device and only take a short look into the code.

So you have a point that should be handled better.

@rdmitry0911

Copy link
Copy Markdown
Author

Agree the measurement request could be smarter, and that's worth improving on its own.

It won't fix this case though. The iPhones here advertise rrm=0x42 — Neighbor Report + Beacon Table only, no active/passive beacon measurement (and mbo:false). So however the request is phrased, a client camped on 6 GHz just answers from its beacon table, which has no 5/2.4 GHz entries because it isn't scanning them. usteer then reaches SCAN_DONE with no lower-band candidate and gives up, so the client stays on the dying 6 GHz.

That's the only case band_downsteer handles: no measured candidate -> directed BSS-TM to the same AP one band down (guaranteed reachable). Measured roaming keeps priority.

I've got a 6 GHz setup here (4x MT7996), so I can send the get_clients rrm dump and a usteer debug log of that SCAN_DONE/no-candidate path if it helps, since you can't test it yourself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants