feat: env check auto-disables dummy_radio0 STA (2.4GHz recon root cause)

The stock STA client interface (wlan0) holds phy0's channel, pinning wlan0mon
so 2.4GHz recon captures nothing (verified: iw set channel -> Resource busy
until wlan0 is down). env_check now disables it (uci wireless.dummy_radio0
disabled=1 + wifi reload) at startup and /api/recon/status exposes wlan0_sta
so the recon page can warn if it regresses.
This commit is contained in:
2026-08-19 10:42:26 -05:00
parent 5eb81b90f1
commit 5a72566381
3 changed files with 55 additions and 2 deletions
@@ -1359,6 +1359,7 @@ def h_recon_status(ctx):
'scanning': scanning, 'scan_remaining': remaining, 'stale': stale,
'hopper_online': _hopper_online(),
'wlan0_pinned': _wlan0_pinned(),
'wlan0_sta': _sta_uplink_enabled(),
'wlan0_hopping': _wlan0_hopping(),
'history_reset': _recon_history_reset()}
@@ -3717,6 +3718,20 @@ def _wlan0_pinned():
return False
def _sta_uplink_enabled():
"""True when the stock dummy_radio0 STA client interface is enabled. The
STA holds phy0's channel, which pins wlan0mon and starves 2.4GHz recon
entirely (iw set channel fails with Resource busy while it is up)."""
cfg = _uci_values('wireless.dummy_radio0') or {}
return cfg.get('mode') == 'sta' and cfg.get('disabled') != '1'
def _disable_sta_uplink():
device_run(['uci', 'set', 'wireless.dummy_radio0.disabled=1'])
device_run(['uci', 'commit', 'wireless'])
device_run(['wifi', 'reload'], timeout=30)
def _pineap_interfaces():
"""Runtime pineapd interface table from ``_pineap INTERFACE LIST``:
{name: {'channels': int, 'bands': str, 'hop': str, 'pkts': int}}."""
@@ -3790,6 +3805,13 @@ def env_check():
else:
_env_step(report, 'warn', pool_detail)
if _sta_uplink_enabled():
_disable_sta_uplink()
_env_step(report, 'fixed', '2.4GHz recon: dummy_radio0 STA uplink disabled '
'(it was pinning phy0 so wlan0mon could not hop)')
else:
_env_step(report, 'pass', 'no STA uplink pinning phy0')
raised = _raise_monitors()
if raised:
_env_step(report, 'fixed', 'monitor interfaces brought up', ', '.join(raised))
@@ -1462,7 +1462,7 @@ views.recon = (root) => {
compare: [], history: {}, mapBand: null,
archive: null, archives: [], scanRemaining: null,
hopperOnline: null, historyReset: false, scanErr: null,
wlan0Pinned: false, wlan0Hopping: null };
wlan0Pinned: false, wlan0Sta: false, wlan0Hopping: null };
const cols = reconLoadCols();
// ---- title cards (stat cards with optional mini charts) ----
@@ -1661,10 +1661,11 @@ views.recon = (root) => {
if (state.hopperOnline === false) bits.push('Hopper radio offline — fewer networks seen');
if (state.historyReset) bits.push('History reset — previous scans archived (see Previous Scans)');
if (state.wlan0Pinned) bits.push('2.4GHz under-sampled — OpenAP/Evil WPA holds wlan0mon');
if (state.wlan0Sta) bits.push('2.4GHz starved — client STA (wlan0) pins phy0');
if (state.wlan0Hopping === false) bits.push('2.4GHz starved — wlan0mon hopping is off');
if (state.scanErr) bits.push(state.scanErr);
scanStatus.textContent = bits.join(' · ');
scanStatus.classList.toggle('warn', state.hopperOnline === false || state.historyReset || state.wlan0Pinned || state.wlan0Hopping === false || !!state.scanErr);
scanStatus.classList.toggle('warn', state.hopperOnline === false || state.historyReset || state.wlan0Pinned || state.wlan0Sta || state.wlan0Hopping === false || !!state.scanErr);
}
scanToggle.addEventListener('change', () => {
if (pendingScan) { scanToggle.checked = !scanToggle.checked; return; }
@@ -2527,6 +2528,7 @@ views.recon = (root) => {
state.hopperOnline = r.data.hopper_online;
state.historyReset = !!r.data.history_reset;
state.wlan0Pinned = !!r.data.wlan0_pinned;
state.wlan0Sta = !!r.data.wlan0_sta;
state.wlan0Hopping = r.data.wlan0_hopping;
if (!pendingScan) scanToggle.checked = scanning;
renderScanBar();