fix(phase26): dual-radio — wlan0 permanent AP, wlan1 internet uplink (BUG-01)
This commit is contained in:
+30
-47
@@ -199,32 +199,19 @@ def write_status(action, error=None):
|
||||
|
||||
|
||||
def restore_fallback():
|
||||
"""Try home-wifi on wlan1. wlan0 AP is permanent and never touched."""
|
||||
if activate(HOME_CONNECTION):
|
||||
write_status("connected_home")
|
||||
return True
|
||||
# If the primary interface (wlan1) failed, try the wlan0 fallback profiles
|
||||
if INTERFACE == PRIMARY_INTERFACE and _interface_exists(FALLBACK_INTERFACE):
|
||||
# Temporarily try connections bound to wlan0
|
||||
for conn in (f"{HOME_CONNECTION}-fb", AP_CONNECTION):
|
||||
proc = subprocess.run(
|
||||
["nmcli", "--wait", "30", "connection", "up", conn],
|
||||
capture_output=True, text=True, timeout=35,
|
||||
)
|
||||
if proc.returncode == 0:
|
||||
write_status(f"fallback_wlan0_{conn}")
|
||||
return True
|
||||
if activate(AP_CONNECTION):
|
||||
write_status("restored_local_hotspot")
|
||||
return True
|
||||
write_status("recovery_failed", "Could not activate home WiFi or local hotspot.")
|
||||
return False
|
||||
write_status("no_connection", "No home WiFi or phone hotspot available.")
|
||||
return True
|
||||
|
||||
|
||||
def _ap_client_count():
|
||||
"""Return number of stations connected to the AP on INTERFACE."""
|
||||
"""Return number of stations connected to the AP on wlan0."""
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["iw", "dev", INTERFACE, "station", "dump"],
|
||||
["iw", "dev", "wlan0", "station", "dump"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
return proc.stdout.count("Station ")
|
||||
@@ -233,15 +220,10 @@ def _ap_client_count():
|
||||
|
||||
|
||||
def check_uplink():
|
||||
"""Manage internet uplink on wlan1 only. wlan0 is a permanent AP, never touched."""
|
||||
current = active_connection()
|
||||
phone_configured = connection_exists(PHONE_CONNECTION)
|
||||
|
||||
# Never tear down the AP if a client is connected — the user is actively
|
||||
# using the hotspot (e.g. phone browsing the web UI while driving).
|
||||
# Skip this netwatch cycle and let the next one try again.
|
||||
if current == AP_CONNECTION and _ap_client_count() > 0:
|
||||
write_status("ap_has_clients_skipped_check")
|
||||
return True
|
||||
if current == PHONE_CONNECTION:
|
||||
if internet_reachable():
|
||||
write_status("phone_connected")
|
||||
@@ -255,18 +237,20 @@ def check_uplink():
|
||||
"Phone hotspot is connected but internet is not reachable.",
|
||||
)
|
||||
return True
|
||||
return restore_fallback()
|
||||
return _connect_home_or_idle()
|
||||
|
||||
if phone_configured:
|
||||
if current == AP_CONNECTION:
|
||||
run_nmcli("connection", "down", AP_CONNECTION, check=False)
|
||||
# Only take down the phone hotspot if it's up — never touch the AP on wlan0
|
||||
was_phone = current == PHONE_CONNECTION
|
||||
if was_phone:
|
||||
run_nmcli("connection", "down", PHONE_CONNECTION, check=False)
|
||||
run_nmcli("device", "wifi", "rescan", "ifname", INTERFACE, check=False)
|
||||
time.sleep(5)
|
||||
if not phone_is_visible():
|
||||
if current != AP_CONNECTION and active_connection():
|
||||
if not was_phone and active_connection():
|
||||
write_status("phone_not_in_range_current_connection_preserved")
|
||||
return True
|
||||
return restore_fallback()
|
||||
return _connect_home_or_idle()
|
||||
if activate(PHONE_CONNECTION):
|
||||
time.sleep(3)
|
||||
if internet_reachable():
|
||||
@@ -281,20 +265,28 @@ def check_uplink():
|
||||
"Phone hotspot is connected but internet is not reachable.",
|
||||
)
|
||||
return True
|
||||
return restore_fallback()
|
||||
return _connect_home_or_idle()
|
||||
if active_connection() == HOME_CONNECTION:
|
||||
write_status("phone_unavailable_home_preserved")
|
||||
return True
|
||||
return restore_fallback()
|
||||
return _connect_home_or_idle()
|
||||
|
||||
if current != AP_CONNECTION:
|
||||
write_status("phone_not_configured")
|
||||
# Phone not configured. Home wifi is the only uplink on wlan1.
|
||||
if current == HOME_CONNECTION:
|
||||
write_status("home_connected_no_phone")
|
||||
return True
|
||||
|
||||
run_nmcli("connection", "down", AP_CONNECTION, check=False)
|
||||
run_nmcli("device", "wifi", "rescan", "ifname", INTERFACE, check=False)
|
||||
time.sleep(5)
|
||||
return restore_fallback()
|
||||
# Not on home-wifi — try to connect
|
||||
return _connect_home_or_idle()
|
||||
|
||||
|
||||
def _connect_home_or_idle():
|
||||
"""Try home-wifi on wlan1. wlan0 AP is never touched."""
|
||||
if activate(HOME_CONNECTION):
|
||||
write_status("connected_home")
|
||||
return True
|
||||
write_status("no_uplink_available", "No home WiFi or phone hotspot in range.")
|
||||
return True
|
||||
|
||||
|
||||
def wait_for_phone(timeout=120):
|
||||
@@ -304,11 +296,8 @@ def wait_for_phone(timeout=120):
|
||||
write_status("phone_connected")
|
||||
return True
|
||||
|
||||
original_connection = active_connection()
|
||||
write_status("waiting_for_phone")
|
||||
time.sleep(10)
|
||||
if original_connection == AP_CONNECTION:
|
||||
run_nmcli("connection", "down", AP_CONNECTION, check=False)
|
||||
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
@@ -317,13 +306,7 @@ def wait_for_phone(timeout=120):
|
||||
if phone_is_visible():
|
||||
return check_uplink()
|
||||
|
||||
if original_connection != AP_CONNECTION and active_connection():
|
||||
write_status(
|
||||
"phone_wait_timed_out_current_connection_preserved",
|
||||
"Phone hotspot did not appear within two minutes.",
|
||||
)
|
||||
return True
|
||||
restored = restore_fallback()
|
||||
restored = _connect_home_or_idle()
|
||||
if restored:
|
||||
write_status(
|
||||
"phone_wait_timed_out_fallback_restored",
|
||||
|
||||
+27
-9
@@ -134,17 +134,35 @@ def get_status():
|
||||
connectivity = values[2]
|
||||
phone_ssid = _phone_ssid()
|
||||
internet_reachable = connectivity == "4 (full)"
|
||||
|
||||
# Check wlan0 AP state (permanent hotspot)
|
||||
ap_active = False
|
||||
ap_ip = ""
|
||||
try:
|
||||
ap_active = subprocess.run(
|
||||
["nmcli", "-g", "GENERAL.CONNECTION", "device", "show", "wlan0"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
).stdout.strip() == "Hotspot-local"
|
||||
if ap_active:
|
||||
ap_ip = subprocess.run(
|
||||
["nmcli", "-g", "IP4.ADDRESS", "device", "show", "wlan0"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
).stdout.strip().split("/", 1)[0]
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
pass
|
||||
|
||||
# Mode reflects wlan1 uplink state, not wlan0 AP
|
||||
uplink_mode = (
|
||||
"phone" if active == PHONE_CONNECTION
|
||||
else "wifi" if active
|
||||
else "disconnected"
|
||||
)
|
||||
|
||||
return {
|
||||
"active_connection": active,
|
||||
"mode": (
|
||||
"phone"
|
||||
if active == PHONE_CONNECTION
|
||||
else "local_hotspot"
|
||||
if active == "Hotspot"
|
||||
else "wifi"
|
||||
if active
|
||||
else "disconnected"
|
||||
),
|
||||
"mode": uplink_mode,
|
||||
"ap_active": ap_active,
|
||||
"ap_ip": ap_ip,
|
||||
"state": values[1],
|
||||
"internet_reachable": internet_reachable,
|
||||
"connectivity": connectivity,
|
||||
|
||||
@@ -255,13 +255,12 @@ async function loadUplinkStatus() {
|
||||
const disconnectedInfo = document.getElementById('in-car-disconnected-info');
|
||||
const hotspotUrl = document.getElementById('hotspot-url');
|
||||
const copyBtn = document.getElementById('copy-hotspot-url');
|
||||
if (status.mode === 'local_hotspot') {
|
||||
const ip = status.ip_address || '10.42.0.1';
|
||||
hotspotUrl.textContent = `http://${ip}`;
|
||||
if (status.ap_active && status.ap_ip) {
|
||||
hotspotUrl.textContent = `http://${status.ap_ip}`;
|
||||
hotspotInfo.hidden = false;
|
||||
wifiInfo.hidden = true;
|
||||
disconnectedInfo.hidden = true;
|
||||
if (copyBtn) copyBtn.onclick = () => navigator.clipboard.writeText(`http://${ip}`);
|
||||
if (copyBtn) copyBtn.onclick = () => navigator.clipboard.writeText(`http://${status.ap_ip}`);
|
||||
} else if (status.mode === 'wifi' || status.mode === 'phone') {
|
||||
hotspotInfo.hidden = true;
|
||||
wifiInfo.hidden = false;
|
||||
|
||||
Reference in New Issue
Block a user