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():
|
def restore_fallback():
|
||||||
|
"""Try home-wifi on wlan1. wlan0 AP is permanent and never touched."""
|
||||||
if activate(HOME_CONNECTION):
|
if activate(HOME_CONNECTION):
|
||||||
write_status("connected_home")
|
write_status("connected_home")
|
||||||
return True
|
return True
|
||||||
# If the primary interface (wlan1) failed, try the wlan0 fallback profiles
|
write_status("no_connection", "No home WiFi or phone hotspot available.")
|
||||||
if INTERFACE == PRIMARY_INTERFACE and _interface_exists(FALLBACK_INTERFACE):
|
return True
|
||||||
# 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
|
|
||||||
|
|
||||||
|
|
||||||
def _ap_client_count():
|
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:
|
try:
|
||||||
proc = subprocess.run(
|
proc = subprocess.run(
|
||||||
["iw", "dev", INTERFACE, "station", "dump"],
|
["iw", "dev", "wlan0", "station", "dump"],
|
||||||
capture_output=True, text=True, timeout=5,
|
capture_output=True, text=True, timeout=5,
|
||||||
)
|
)
|
||||||
return proc.stdout.count("Station ")
|
return proc.stdout.count("Station ")
|
||||||
@@ -233,15 +220,10 @@ def _ap_client_count():
|
|||||||
|
|
||||||
|
|
||||||
def check_uplink():
|
def check_uplink():
|
||||||
|
"""Manage internet uplink on wlan1 only. wlan0 is a permanent AP, never touched."""
|
||||||
current = active_connection()
|
current = active_connection()
|
||||||
phone_configured = connection_exists(PHONE_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 current == PHONE_CONNECTION:
|
||||||
if internet_reachable():
|
if internet_reachable():
|
||||||
write_status("phone_connected")
|
write_status("phone_connected")
|
||||||
@@ -255,18 +237,20 @@ def check_uplink():
|
|||||||
"Phone hotspot is connected but internet is not reachable.",
|
"Phone hotspot is connected but internet is not reachable.",
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
return restore_fallback()
|
return _connect_home_or_idle()
|
||||||
|
|
||||||
if phone_configured:
|
if phone_configured:
|
||||||
if current == AP_CONNECTION:
|
# Only take down the phone hotspot if it's up — never touch the AP on wlan0
|
||||||
run_nmcli("connection", "down", AP_CONNECTION, check=False)
|
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)
|
run_nmcli("device", "wifi", "rescan", "ifname", INTERFACE, check=False)
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
if not phone_is_visible():
|
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")
|
write_status("phone_not_in_range_current_connection_preserved")
|
||||||
return True
|
return True
|
||||||
return restore_fallback()
|
return _connect_home_or_idle()
|
||||||
if activate(PHONE_CONNECTION):
|
if activate(PHONE_CONNECTION):
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
if internet_reachable():
|
if internet_reachable():
|
||||||
@@ -281,20 +265,28 @@ def check_uplink():
|
|||||||
"Phone hotspot is connected but internet is not reachable.",
|
"Phone hotspot is connected but internet is not reachable.",
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
return restore_fallback()
|
return _connect_home_or_idle()
|
||||||
if active_connection() == HOME_CONNECTION:
|
if active_connection() == HOME_CONNECTION:
|
||||||
write_status("phone_unavailable_home_preserved")
|
write_status("phone_unavailable_home_preserved")
|
||||||
return True
|
return True
|
||||||
return restore_fallback()
|
return _connect_home_or_idle()
|
||||||
|
|
||||||
if current != AP_CONNECTION:
|
# Phone not configured. Home wifi is the only uplink on wlan1.
|
||||||
write_status("phone_not_configured")
|
if current == HOME_CONNECTION:
|
||||||
|
write_status("home_connected_no_phone")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
run_nmcli("connection", "down", AP_CONNECTION, check=False)
|
# Not on home-wifi — try to connect
|
||||||
run_nmcli("device", "wifi", "rescan", "ifname", INTERFACE, check=False)
|
return _connect_home_or_idle()
|
||||||
time.sleep(5)
|
|
||||||
return restore_fallback()
|
|
||||||
|
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):
|
def wait_for_phone(timeout=120):
|
||||||
@@ -304,11 +296,8 @@ def wait_for_phone(timeout=120):
|
|||||||
write_status("phone_connected")
|
write_status("phone_connected")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
original_connection = active_connection()
|
|
||||||
write_status("waiting_for_phone")
|
write_status("waiting_for_phone")
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
if original_connection == AP_CONNECTION:
|
|
||||||
run_nmcli("connection", "down", AP_CONNECTION, check=False)
|
|
||||||
|
|
||||||
deadline = time.monotonic() + timeout
|
deadline = time.monotonic() + timeout
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
@@ -317,13 +306,7 @@ def wait_for_phone(timeout=120):
|
|||||||
if phone_is_visible():
|
if phone_is_visible():
|
||||||
return check_uplink()
|
return check_uplink()
|
||||||
|
|
||||||
if original_connection != AP_CONNECTION and active_connection():
|
restored = _connect_home_or_idle()
|
||||||
write_status(
|
|
||||||
"phone_wait_timed_out_current_connection_preserved",
|
|
||||||
"Phone hotspot did not appear within two minutes.",
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
restored = restore_fallback()
|
|
||||||
if restored:
|
if restored:
|
||||||
write_status(
|
write_status(
|
||||||
"phone_wait_timed_out_fallback_restored",
|
"phone_wait_timed_out_fallback_restored",
|
||||||
|
|||||||
+27
-9
@@ -134,17 +134,35 @@ def get_status():
|
|||||||
connectivity = values[2]
|
connectivity = values[2]
|
||||||
phone_ssid = _phone_ssid()
|
phone_ssid = _phone_ssid()
|
||||||
internet_reachable = connectivity == "4 (full)"
|
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 {
|
return {
|
||||||
"active_connection": active,
|
"active_connection": active,
|
||||||
"mode": (
|
"mode": uplink_mode,
|
||||||
"phone"
|
"ap_active": ap_active,
|
||||||
if active == PHONE_CONNECTION
|
"ap_ip": ap_ip,
|
||||||
else "local_hotspot"
|
|
||||||
if active == "Hotspot"
|
|
||||||
else "wifi"
|
|
||||||
if active
|
|
||||||
else "disconnected"
|
|
||||||
),
|
|
||||||
"state": values[1],
|
"state": values[1],
|
||||||
"internet_reachable": internet_reachable,
|
"internet_reachable": internet_reachable,
|
||||||
"connectivity": connectivity,
|
"connectivity": connectivity,
|
||||||
|
|||||||
@@ -255,13 +255,12 @@ async function loadUplinkStatus() {
|
|||||||
const disconnectedInfo = document.getElementById('in-car-disconnected-info');
|
const disconnectedInfo = document.getElementById('in-car-disconnected-info');
|
||||||
const hotspotUrl = document.getElementById('hotspot-url');
|
const hotspotUrl = document.getElementById('hotspot-url');
|
||||||
const copyBtn = document.getElementById('copy-hotspot-url');
|
const copyBtn = document.getElementById('copy-hotspot-url');
|
||||||
if (status.mode === 'local_hotspot') {
|
if (status.ap_active && status.ap_ip) {
|
||||||
const ip = status.ip_address || '10.42.0.1';
|
hotspotUrl.textContent = `http://${status.ap_ip}`;
|
||||||
hotspotUrl.textContent = `http://${ip}`;
|
|
||||||
hotspotInfo.hidden = false;
|
hotspotInfo.hidden = false;
|
||||||
wifiInfo.hidden = true;
|
wifiInfo.hidden = true;
|
||||||
disconnectedInfo.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') {
|
} else if (status.mode === 'wifi' || status.mode === 'phone') {
|
||||||
hotspotInfo.hidden = true;
|
hotspotInfo.hidden = true;
|
||||||
wifiInfo.hidden = false;
|
wifiInfo.hidden = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user